use std::cmp::Ordering;
use std::collections::binary_heap::PeekMut;
use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};
use std::mem::take;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{fmt, io, iter};

use anyhow::{Context as ErrorContext, Result, anyhow, bail};
use chrono::{DateTime, Utc};
use futures::channel::mpsc;
use futures::{Future, FutureExt};
use indexmap::IndexMap;
use irc::proto::{self, Command, Tags, command, tags};
use itertools::{Either, Itertools};
use tokio::fs;

pub use self::on_connect::on_connect;
use crate::bouncer::{self, BouncerNetwork};
use crate::capabilities::{
    self, Capabilities, Capability, LabeledResponseContext, MultilineBatchKind,
    MultilineLimits, chathistory_entry_server_time, multiline_concat_lines,
    multiline_encoded,
};
use crate::config::server::filehost;
use crate::environment::{SOURCE_WEBSITE, VERSION};
use crate::features::{self, Features, VersionRequest};
use crate::history::ReadMarker;
use crate::isupport::{
    ChatHistoryState, ChatHistorySubcommand, MessageReference, WhoToken,
    WhoXPollParameters, find_target_limit, format_optional_message_reference,
};
use crate::message::source;
use crate::rate_limit::{BackoffInterval, TokenBucket, TokenPriority};
use crate::target::{self, Target};
use crate::time::Posix;
use crate::user::{ChannelUsers, Nick, NickRef};
use crate::{
    Server, User, buffer, channel_discovery, compression, config, ctcp, dcc,
    environment, file_transfer, fileupload, history, isupport, message,
    metadata, mode, server,
};

pub mod on_connect;

const HIGHLIGHT_BLACKOUT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_CHATHISTORY_LIMIT: u16 = 1000;
const CHATHISTORY_REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
const MODE_REQUEST_DELAY: Duration = Duration::from_millis(600);
const MODE_REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
const TYPING_TIMEOUT: Duration = Duration::from_secs(6);

#[derive(Debug, Clone, Copy)]
pub enum Status {
    Unavailable,
    Connected,
    Disconnected,
}

impl Status {
    pub fn connected(&self) -> bool {
        matches!(self, Status::Connected)
    }
}

#[derive(Debug)]
pub enum State {
    Disconnected,
    Ready(Client),
}

#[derive(Debug)]
pub enum Broadcast {
    Quit {
        user: User,
        comment: Option<String>,
        channels: Vec<target::Channel>,
        server_time: DateTime<Utc>,
        received_with_server_time: bool,
    },
    Nickname {
        old_user: User,
        new_nick: Nick,
        ourself: bool,
        channels: Vec<target::Channel>,
        server_time: DateTime<Utc>,
        received_with_server_time: bool,
    },
    ChangeHost {
        old_user: User,
        new_username: String,
        new_hostname: String,
        ourself: bool,
        logged_in: bool,
        channels: Vec<target::Channel>,
        server_time: DateTime<Utc>,
        received_with_server_time: bool,
    },
    Kick {
        kicker: User,
        victim: User,
        reason: Option<String>,
        channel: target::Channel,
        server_time: DateTime<Utc>,
        received_with_server_time: bool,
    },
}

#[derive(Debug)]
pub enum Destination {
    Server,
    Target(Target),
}

impl From<&buffer::Upstream> for Destination {
    fn from(buffer: &buffer::Upstream) -> Self {
        match buffer.target() {
            Some(target) => Self::Target(target),
            None => Self::Server,
        }
    }
}

impl From<Target> for Destination {
    fn from(target: Target) -> Self {
        Self::Target(target)
    }
}

#[derive(Debug)]
pub enum Message {
    ChatHistoryRequest(Server, ChatHistorySubcommand),
    ChatHistoryTargetsTimestampUpdated(
        Server,
        DateTime<Utc>,
        Result<(), Error>,
    ),
    RequestNewerChatHistory(Server, Target, DateTime<Utc>, bool),
}

#[derive(Debug)]
pub enum Event {
    Single {
        message: message::Encoded,
        our_nick: Nick,
        deduplicate: bool,
    },
    PrivOrNotice {
        message: message::Encoded,
        our_nick: Nick,
        notification_enabled: bool,
        deduplicate: bool,
        labeled_response_context: Option<LabeledResponseContext>,
    },
    Reaction {
        message: message::Encoded,
        our_nick: Nick,
        notification_enabled: bool,
        deduplicate: bool,
        labeled_response_context: Option<LabeledResponseContext>,
    },
    Redaction(message::Encoded, Nick),
    WithTarget {
        message: message::Encoded,
        our_nick: Nick,
        target: Destination,
        deduplicate: bool,
    },
    Broadcast(Broadcast),
    FileTransferRequest(file_transfer::ReceiveRequest),
    UpdateReadMarker(Target, ReadMarker),
    JoinedChannel(target::Channel, DateTime<Utc>),
    LoggedIn(DateTime<Utc>),
    AddedIsupportParam(isupport::Parameter),
    ChatHistoryTargetReceived(Target, DateTime<Utc>),
    ChatHistoryTargetsReceived(DateTime<Utc>),
    DirectMessage(message::Encoded, Nick, User),
    MonitoredOnline(Vec<User>),
    MonitoredOffline(Vec<Nick>),
    OnConnect(on_connect::Stream),
    BouncerNetwork(Server, config::Server),
    AddToSidebar(target::Query),
    Disconnect(Option<String>),
    UpdateIcon,
}

struct ChatHistoryRequest {
    subcommand: ChatHistorySubcommand,
    requested_at: Instant,
    autorequest: bool,
}

#[derive(Clone, Debug)]
struct MonitoredUser {
    online: bool,
    automated: bool,
}

pub struct Client {
    server: Server,
    config: Arc<config::Server>,
    handle: server::Handle,
    alt_nick: Option<usize>,
    resolved_nick: Option<Nick>,
    resolved_user: Option<String>,
    resolved_host: Option<String>,
    configured_nick: Nick,
    chanmap: IndexMap<target::Channel, Channel>,
    querymap: IndexMap<target::Query, QueryState>,
    labels: HashMap<String, Context>,
    batches: HashMap<String, Batch>,
    reroute_responses_to: Option<buffer::Upstream>,
    logged_in: bool,
    registration_step: RegistrationStep,
    capabilities: Capabilities,
    features: Features,
    sasl_succeeded: bool,
    pending_chathistory_requests:
        HashMap<Target, (ChatHistorySubcommand, TokenPriority)>,
    chathistory_requests: HashMap<Target, ChatHistoryRequest>,
    chathistory_exhausted: HashMap<Target, bool>,
    chathistory_targets_request: Option<ChatHistoryRequest>,
    notification_blackout: NotificationBlackout,
    registration_required_channels: Vec<target::Channel>,
    isupport: HashMap<isupport::Kind, isupport::Parameter>,
    who_polls: VecDeque<WhoPoll>,
    who_poll_interval: BackoffInterval,
    resolved_netid: Option<String>,
    anti_flood: Option<TokenBucket<message::Encoded>>,
    mode_requests: Vec<ModeRequest>,
    metadata_sub_requests: HashSet<String>,
    metadata_syncs: BinaryHeap<MetadataSync>,
    channel_discovery_manager: channel_discovery::Manager,
    http_client: Option<Arc<reqwest::Client>>, // Only Some if config.proxy.is_some()
    registry: metadata::ServerRegistry,
    monitored_users: HashMap<User, MonitoredUser>,
}

impl fmt::Debug for Client {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Client").finish()
    }
}

impl Client {
    pub fn new(
        server: Server,
        config: Arc<config::Server>,
        sender: mpsc::Sender<proto::Message>,
    ) -> Self {
        // If config.proxy.is_none() then the default HTTP client will be used,
        // in that case http_client can be None
        let http_client = config.proxy.as_ref().and_then(|proxy| {
            match config::proxy::build_client(Some(proxy), None) {
                Ok(http_client) => Some(http_client),
                Err(error) => {
                    log::warn!("[{server}] Unable to build HTTP client, preview fetching and file upload disabled: {error}");

                    None
                }
            }
        });

        Self {
            server,
            handle: sender,
            resolved_nick: None,
            resolved_user: None,
            resolved_host: None,
            configured_nick: Nick::from_str(
                &config.nickname,
                isupport::CaseMap::default(),
            ),
            alt_nick: None,
            chanmap: IndexMap::default(),
            querymap: IndexMap::default(),
            labels: HashMap::new(),
            batches: HashMap::new(),
            reroute_responses_to: None,
            logged_in: false,
            registration_step: RegistrationStep::Start,
            capabilities: Capabilities::default(),
            features: Features::default(),
            sasl_succeeded: false,
            pending_chathistory_requests: HashMap::new(),
            chathistory_requests: HashMap::new(),
            chathistory_exhausted: HashMap::new(),
            chathistory_targets_request: None,
            notification_blackout: NotificationBlackout::Blackout(
                Instant::now(),
            ),
            registration_required_channels: vec![],
            isupport: HashMap::new(),
            who_polls: VecDeque::new(),
            who_poll_interval: BackoffInterval::from(
                config
                    .who_poll_interval
                    .min(config.anti_flood.saturating_mul(2)),
            ),
            resolved_netid: None,
            anti_flood: Some(TokenBucket::new(config.anti_flood, 10)),
            mode_requests: Vec::new(),
            metadata_sub_requests: HashSet::new(),
            metadata_syncs: BinaryHeap::new(),
            http_client: http_client.map(Arc::new),
            config,
            channel_discovery_manager: channel_discovery::Manager::new(),
            registry: metadata::ServerRegistry::new(),
            monitored_users: HashMap::new(),
        }
    }

    // For each bouncer, we reserve a primary (unbound) TCP connection for bouncer communication.
    // This function returns true if we are that connection.
    //
    // The curious reader may wonder why we store the netID twice. The answer is that the netID in
    // `self.server` is the netID that we are _requesting_, while `resolved_netid` is the netID
    // that is received. Even if we do not request to be bound to a network, we may be bound
    // nonetheless. For example, this happens in soju when one uses a `user/network` username.
    //
    // If we realize the server we connected to is bound, we could try to update this `Server`
    // across the halloy structures... but this would be very difficult and error prone. Instead in
    // this file we simply accept being a non-primary connection and forego any bouncer
    // communication.
    //
    // If !is_primary holds, then resolved_netid and server.bouncer_netid() must match.
    fn is_primary(&self) -> bool {
        !self.server.is_bouncer_network() && self.resolved_netid.is_none()
    }

    pub fn connect(&mut self) -> Result<()> {
        // Begin registration
        self.handle.try_send(command!("CAP", "LS", "302"))?;

        // Identify
        let nick = &self.config.nickname;
        let user = self.config.username.as_ref().unwrap_or(nick);
        let real = self.config.realname.as_ref().unwrap_or(nick);

        if let Some(pass) = self.config.password.as_ref() {
            self.handle.try_send(command!("PASS", pass))?;
        }
        self.handle.try_send(command!("NICK", nick))?;
        self.handle.try_send(command!("USER", user, real))?;
        self.registration_step = RegistrationStep::List;
        Ok(())
    }

    pub fn update_config(
        &mut self,
        config: Arc<config::Server>,
        from_modal: bool,
    ) -> Vec<Event> {
        if self.registration_step == RegistrationStep::Complete {
            // TODO: allow from_modal when modal matching to bouncer networks is added.
            if !self.capabilities.acknowledged(Capability::BouncerNetworks) {
                self.join(
                    &config
                        .channels
                        .iter()
                        .filter_map(|channel| {
                            target::Channel::parse(
                                channel,
                                self.chantypes(),
                                self.statusmsg(),
                                self.casemapping(),
                            )
                            .ok()
                        })
                        .filter(|channel| !self.chanmap.contains_key(channel))
                        .collect::<Vec<_>>(),
                );
            }

            if !config.monitor.is_empty()
                && config.monitor != self.config.monitor
            {
                if let Some(isupport::Parameter::MONITOR(monitor_limit)) =
                    self.isupport.get(&isupport::Kind::MONITOR)
                {
                    let casemapping =
                        isupport::get_casemapping_or_default(&self.isupport);
                    let prefix = isupport::get_prefix(&self.isupport);

                    let remove_monitors = self
                        .config
                        .monitor
                        .iter()
                        .filter_map(|existing_monitor| {
                            let user = User::parse_or_force(
                                existing_monitor,
                                casemapping,
                                prefix,
                            );
                            if !config.monitor.contains(existing_monitor)
                                && !self.is_monitored_user_automated(&user)
                            {
                                Some(user)
                            } else {
                                None
                            }
                        })
                        .collect::<Vec<_>>();

                    let monitored_users_keys: Vec<User> =
                        self.monitored_users.keys().cloned().collect();
                    for user in monitored_users_keys {
                        if remove_monitors
                            .iter()
                            .any(|remove_monitor| remove_monitor == &user)
                        {
                            self.monitored_users.remove(&user);
                        } else {
                            self.monitored_users.entry(user).and_modify(
                                |monitored_user| {
                                    monitored_user.automated = false;
                                },
                            );
                        }
                    }

                    let mut messages = group_monitors(
                        &config.monitor,
                        *monitor_limit,
                        find_target_limit(&self.isupport, "MONITOR"),
                        &self.server,
                        self.monitored_users.len(),
                    )
                    .collect::<Vec<_>>();

                    if !remove_monitors.is_empty() {
                        messages.push(command!(
                            "MONITOR",
                            "-",
                            remove_monitors
                                .iter()
                                .map(super::user::User::as_normalized_str)
                                .join(",")
                        ));
                    }

                    for message in messages {
                        if let Err(e) = self.handle.try_send(message) {
                            log::warn!(
                                "[{}] Error sending monitor: {e}",
                                self.server
                            );
                        }
                    }
                } else {
                    log::warn!(
                        "[{}] Monitor list configured for, but is not supported by the server",
                        self.server,
                    );
                }
            }

            if (!config.metadata.is_empty() || !self.config.metadata.is_empty())
                && config.metadata != self.config.metadata
            {
                if self.capabilities.acknowledged(Capability::Metadata) {
                    self.send(
                        None,
                        command!(
                            "METADATA",
                            self.nickname().to_string(),
                            "CLEAR"
                        )
                        .into(),
                        TokenPriority::High,
                    );

                    for (key, value) in config.metadata.iter() {
                        self.send(
                            None,
                            command!(
                                "METADATA",
                                self.nickname().to_string(),
                                "SET",
                                key.to_str(),
                                value
                            )
                            .into(),
                            TokenPriority::High,
                        );
                    }
                } else {
                    log::warn!(
                        "[{}] Metadata configured for, but is not supported by the server",
                        self.server,
                    );
                }
            }
        }

        let mut events = config
            .queries
            .iter()
            .filter_map(|query| {
                target::Query::parse(
                    query,
                    self.chantypes(),
                    self.statusmsg(),
                    self.casemapping(),
                )
                .ok()
                .map(Event::AddToSidebar)
            })
            .collect::<Vec<_>>();

        if from_modal {
            return events;
        }

        if self.config.who_poll_enabled != config.who_poll_enabled {
            if config.who_poll_enabled {
                for channel in self.chanmap.keys() {
                    self.who_polls.push_back(WhoPoll {
                        channel: channel.clone(),
                        status: WhoStatus::Joined,
                    });
                }
            } else {
                self.who_polls.retain(|who_poll| {
                    matches!(
                        who_poll.status,
                        WhoStatus::Requested(_, _, _)
                            | WhoStatus::Receiving(_, _)
                    )
                });
            }
        }

        if self.config.order_channels_by != config.order_channels_by
            || self.config.channels != config.channels
        {
            let casemapping = self.casemapping();
            let chantypes = self.chantypes().to_vec();
            self.chanmap.sort_by(|c1, _, c2, _| {
                compare_channels(
                    &config,
                    &chantypes,
                    casemapping,
                    c1.as_normalized_str(),
                    c2.as_normalized_str(),
                )
            });
        }

        if self.config.icon != config.icon {
            events.push(Event::UpdateIcon);
        }

        self.config = config;

        events
    }

    fn quit(&mut self, reason: Option<String>) {
        self.who_polls.retain(|who_poll| {
            matches!(
                who_poll.status,
                WhoStatus::Requested(_, _, _) | WhoStatus::Receiving(_, _)
            )
        });

        if let Err(e) = if let Some(reason) = reason {
            self.handle.try_send(command!("QUIT", reason))
        } else {
            self.handle.try_send(command!("QUIT"))
        } {
            log::warn!("[{}] Error sending quit: {e}", self.server);
        }
    }

    fn join(&mut self, channels: &[target::Channel]) {
        let keys = HashMap::new();

        let messages = group_joins(
            channels,
            &keys,
            find_target_limit(&self.isupport, "JOIN"),
        );

        for message in messages {
            if let Err(e) = self.handle.try_send(message) {
                log::warn!("[{}] Error sending join: {e}", self.server);
            }
        }
    }

    fn start_reroute(&self, command: &Command) -> bool {
        use Command::*;

        if let MODE(target, _, _) = command {
            !self.is_channel(target)
        } else {
            matches!(command, WHO(..) | WHOIS(..) | WHOWAS(..) | INVITE(..))
        }
    }

    fn stop_reroute(&self, message: &message::Encoded) -> bool {
        use command::Numeric::*;

        match &message.command {
            Command::INVITE(..) => message
                .user(self.casemapping())
                .is_some_and(|user| user.nickname() == self.nickname()),
            Command::Numeric(RPL_ENDOFWHO, args) => {
                let mask = args.get(1).cloned().unwrap_or_default();

                if let Ok(target_channel) = target::Channel::parse(
                    &mask,
                    self.chantypes(),
                    self.statusmsg(),
                    self.casemapping(),
                ) {
                    self.user_who_request(&target_channel)
                    // Some servers respond with the mask * instead of the requested
                    // channel name when rate-limiting WHO requests
                } else if mask == "*" {
                    // Either the user requested the mask, in which case the request
                    // should be in who_polls and rerouting should be stopped, or it
                    // is treated as part of a rate-limiting response and if there
                    // are any outstanding user requests rerouting should be stopped.
                    self.who_polls.iter().any(|who_poll| {
                        matches!(
                            who_poll.status,
                            WhoStatus::Requested(WhoSource::User, _, _)
                                | WhoStatus::Receiving(WhoSource::User, _)
                        )
                    })
                } else {
                    let target_channel = target::Channel::from_str(
                        &mask,
                        self.chantypes(),
                        self.casemapping(),
                    );

                    self.user_who_request(&target_channel)
                }
            }
            _ => matches!(
                &message.command,
                Command::Numeric(
                    RPL_ENDOFWHOIS
                        | RPL_ENDOFWHOWAS
                        | ERR_NOSUCHNICK
                        | ERR_NOSUCHSERVER
                        | ERR_NONICKNAMEGIVEN
                        | ERR_WASNOSUCHNICK
                        | ERR_NEEDMOREPARAMS
                        | ERR_USERSDONTMATCH
                        | RPL_UMODEIS
                        | ERR_UMODEUNKNOWNFLAG
                        | RPL_INVITING
                        | ERR_NOSUCHCHANNEL
                        | ERR_NOTONCHANNEL
                        | ERR_CHANOPRIVSNEEDED
                        | ERR_USERONCHANNEL,
                    _
                )
            ),
        }
    }

    fn set_labeled_response_context(
        &mut self,
        buffer: Option<&buffer::Upstream>,
        message: &mut message::Encoded,
    ) -> Option<LabeledResponseContext> {
        buffer.and_then(|buffer| {
            if self.capabilities.acknowledged(Capability::LabeledResponse) {
                let label = generate_label();

                let context =
                    Context::new(message, buffer.clone(), Some(&label));

                let labeled_response_context =
                    context.labeled_response_context().cloned();

                self.labels.insert(label.clone(), context);

                // IRC: Encode tags
                message.tags.insert("label".to_string(), label);

                labeled_response_context
            } else {
                self.reroute_responses_to = self
                    .start_reroute(&message.command)
                    .then(|| buffer.clone());

                None
            }
        })
    }

    fn send(
        &mut self,
        buffer: Option<&buffer::Upstream>,
        mut message: message::Encoded,
        priority: TokenPriority,
    ) -> Option<LabeledResponseContext> {
        let labeled_response_context =
            self.set_labeled_response_context(buffer, &mut message);

        let mut restore_automated_monitored_users: Vec<String> = vec![];

        if matches!(priority, TokenPriority::User) {
            match &message.command {
                Command::LIST(..) => {
                    self.channel_discovery_manager.status =
                        Some(channel_discovery::Status::Requested(Utc::now()));
                }
                Command::WHO(..) => {
                    let params = message.command.clone().parameters();

                    if let Some(mask) = params.first() {
                        let channel = if let Ok(channel) =
                            target::Channel::parse(
                                mask,
                                self.chantypes(),
                                self.statusmsg(),
                                self.casemapping(),
                            ) {
                            Some(channel)
                        } else if mask == "*" {
                            Some(target::Channel::from_str(
                                mask,
                                self.chantypes(),
                                self.casemapping(),
                            ))
                        } else {
                            None
                        };

                        if let Some(channel) = channel {
                            // Record user WHO request(s) for reply filtering
                            let status = WhoStatus::Requested(
                                WhoSource::User,
                                Instant::now(),
                                params.get(2).and_then(|token| {
                                    token.parse::<WhoToken>().ok()
                                }),
                            );

                            if let Some(who_poll) = self
                                .who_polls
                                .iter_mut()
                                .find(|who_poll| who_poll.channel == channel)
                            {
                                who_poll.status = status;
                            } else {
                                self.who_polls
                                    .push_front(WhoPoll { channel, status });
                            }
                        }
                    }
                }
                Command::MONITOR(subcommand, targets) => {
                    match (targets, subcommand.as_str()) {
                        (Some(targets), "+") => {
                            let casemapping =
                                isupport::get_casemapping_or_default(
                                    &self.isupport,
                                );
                            let prefix = isupport::get_prefix(&self.isupport);
                            for target in targets.split(",") {
                                self.monitored_users
                                    .entry(User::parse_or_force(
                                        target,
                                        casemapping,
                                        prefix,
                                    ))
                                    .and_modify(|monitored_user| {
                                        monitored_user.automated = false;
                                    })
                                    .or_insert_with(|| MonitoredUser {
                                        online: false,
                                        automated: false,
                                    });
                            }
                        }
                        (Some(targets), "-") => {
                            let casemapping =
                                isupport::get_casemapping_or_default(
                                    &self.isupport,
                                );
                            let prefix = isupport::get_prefix(&self.isupport);
                            for target in
                                targets.split(',').filter(|s| !s.is_empty())
                            {
                                self.monitored_users.remove(
                                    &User::parse_or_force(
                                        target,
                                        casemapping,
                                        prefix,
                                    ),
                                );
                            }
                        }
                        (_, "C" | "c") => {
                            self.monitored_users.retain(
                                |user, monitored_user| {
                                    if monitored_user.automated {
                                        restore_automated_monitored_users.push(
                                            user.as_normalized_str().to_owned(),
                                        );
                                        true
                                    } else {
                                        false
                                    }
                                },
                            );
                        }
                        _ => (),
                    }
                }
                _ => (),
            }
        }

        if let Some(ref mut anti_flood) = self.anti_flood {
            anti_flood.add_token(message, priority);
        } else if let Err(e) = self.handle.try_send(message.into()) {
            log::warn!("[{}] Error sending message: {e}", self.server);
        }

        if !restore_automated_monitored_users.is_empty()
            && let Some(isupport::Parameter::MONITOR(monitor_limit)) =
                self.isupport.get(&isupport::Kind::MONITOR)
        {
            let messages = group_monitors(
                &restore_automated_monitored_users,
                *monitor_limit,
                find_target_limit(&self.isupport, "MONITOR"),
                &self.server,
                0,
            );
            for message in messages {
                if let Some(ref mut anti_flood) = self.anti_flood {
                    anti_flood.add_token(message.into(), TokenPriority::Low);
                } else if let Err(e) = self.handle.try_send(message) {
                    log::warn!("[{}] Error sending message: {e}", self.server);
                }
            }
        }

        labeled_response_context
    }

    fn send_multiline_batch(
        &mut self,
        buffer: &buffer::Upstream,
        mut messages: Vec<message::Encoded>,
        priority: TokenPriority,
        reply_id: Option<&message::Id>,
    ) -> Option<LabeledResponseContext> {
        if let Some(multiline_limits) = self.multiline_limits()
            && let Some(batch_kind) =
                messages.first().and_then(|message| match message.command {
                    Command::PRIVMSG(_, _) => Some(MultilineBatchKind::PRIVMSG),
                    Command::NOTICE(_, _) => Some(MultilineBatchKind::NOTICE),
                    _ => None,
                })
            && let Some(target) = buffer.target().as_ref()
        {
            let reference_tag = generate_batch_reference_tag();

            let mut opening_batch: message::Encoded = command!(
                "BATCH",
                format!("+{reference_tag}"),
                "draft/multiline",
                target.as_str()
            )
            .into();

            opening_batch.set_reply_to(reply_id);

            let labeled_response_context = self
                .set_labeled_response_context(Some(buffer), &mut opening_batch);

            // Overwrite any existing tags, since messages in the batch can only
            // have draft/multiline-concat and batch tags
            for message in messages.iter_mut() {
                message.tags = tags![ "batch" => reference_tag.clone() ];
            }

            let closing_batch: message::Encoded =
                command!("BATCH", format!("-{reference_tag}")).into();

            let multiline_concat_bytes = multiline_limits.concat_bytes(
                self.relay_bytes(),
                batch_kind,
                target.as_str(),
            );

            let messages = iter::once(opening_batch)
                .chain(messages.into_iter().flat_map(|message| {
                    match &message.command {
                        Command::PRIVMSG(_, text)
                        | Command::NOTICE(_, text) => {
                            let lines = text
                                .lines()
                                .map(|line| {
                                    multiline_concat_lines(
                                        multiline_concat_bytes,
                                        line,
                                    )
                                })
                                .collect::<Vec<_>>();

                            if lines.iter().fold(
                                0,
                                |message_count, concat_lines| {
                                    message_count + concat_lines.len()
                                },
                            ) < 2
                            {
                                vec![message]
                            } else {
                                lines
                                    .into_iter()
                                    .flat_map(|line| {
                                        let mut concat_lines = line
                                            .into_iter()
                                            .map(|text| {
                                                multiline_encoded(
                                                    None,
                                                    batch_kind,
                                                    target,
                                                    text,
                                                    message.tags.clone(),
                                                )
                                            })
                                            .collect::<Vec<_>>();

                                        for concat_line in
                                            concat_lines.iter_mut().skip(1)
                                        {
                                            concat_line.tags.insert(
                                                "draft/multiline-concat"
                                                    .to_string(),
                                                String::new(),
                                            );
                                        }

                                        concat_lines
                                    })
                                    .collect()
                            }
                        }
                        _ => vec![],
                    }
                }))
                .chain(iter::once(closing_batch))
                .collect::<Vec<message::Encoded>>();

            if let Some(ref mut anti_flood) = self.anti_flood {
                for message in messages {
                    anti_flood.add_token(message, priority);
                }
            } else {
                for message in messages {
                    if let Err(e) = self.handle.try_send(message.into()) {
                        log::warn!(
                            "[{}] Error sending message: {e}",
                            self.server
                        );
                    }
                }
            }

            labeled_response_context
        } else {
            log::warn!(
                "[{}] unable to send messages as IRCv3 draft/multiline batch; capability not supported: {messages:?}",
                self.server
            );

            None
        }
    }

    fn receive(
        &mut self,
        message: message::Encoded,
        config: &config::Config,
    ) -> Result<Vec<Event>> {
        log::trace!("[{}] Message received => {:?}", self.server, *message);

        let stop_reroute = self.stop_reroute(&message);

        let events = self.handle(message, None, config)?;

        if stop_reroute {
            self.reroute_responses_to = None;
        }

        Ok(events)
    }

    fn handle(
        &mut self,
        mut message: message::Encoded,
        parent_context: Option<Context>,
        config: &config::Config,
    ) -> Result<Vec<Event>> {
        use irc::proto::command::Numeric::*;

        let label_tag = message.tags.remove("label");

        let batch_tag = message.tags.remove("batch");

        let context = parent_context.or(label_tag
            .as_ref()
            // Remove context associated to label if we get resp for it
            .and_then(|label| self.labels.remove(label))
            // Otherwise if we're in a batch, get its context
            .or(batch_tag.as_ref().and_then(|batch| {
                self.batches
                    .get(batch)
                    .and_then(|batch| batch.context.clone())
            })));

        macro_rules! ok {
            ($option:expr) => {
                $option.ok_or_else(|| {
                    anyhow!(
                        "[{}] Malformed command: {:?}",
                        self.server,
                        message.command
                    )
                })?
            };
        }

        macro_rules! context {
            ($result:expr) => {
                $result.with_context(|| {
                    anyhow!(
                        "[{}] Malformed command: {:?}",
                        self.server,
                        message.command
                    )
                })?
            };
        }

        match &message.command {
            Command::BATCH(batch, params) => {
                let mut chars = batch.chars();
                let symbol = ok!(chars.next());
                let reference = chars.collect::<String>();

                match symbol {
                    '+' => {
                        let mut batch = Batch::new(context);

                        batch.chathistory_end =
                            message.tags.contains_key("draft/chathistory-end");

                        batch.kind = match params.first().map(String::as_str) {
                            Some("chathistory") => {
                                params.get(1).map(|target| {
                                    BatchKind::ChathistoryTarget(Target::parse(
                                        target,
                                        self.chantypes(),
                                        self.statusmsg(),
                                        self.casemapping(),
                                    ))
                                })
                            }
                            Some("draft/chathistory-targets") => {
                                Some(BatchKind::ChathistoryTargets)
                            }
                            Some("draft/multiline") => {
                                params.get(1).map(|target| {
                                    let mut tags = message.tags.clone();
                                    if let Some(label_tag) = label_tag {
                                        tags.insert(
                                            "label".to_string(),
                                            label_tag,
                                        );
                                    }
                                    if let Some(batch_tag) = batch_tag {
                                        tags.insert(
                                            "batch".to_string(),
                                            batch_tag,
                                        );
                                    }
                                    BatchKind::Multiline(
                                        tags,
                                        message.user(self.casemapping()),
                                        Target::parse(
                                            target,
                                            self.chantypes(),
                                            self.statusmsg(),
                                            self.casemapping(),
                                        ),
                                        None,
                                        String::new(),
                                    )
                                })
                            }
                            Some("znc.in/playback") => {
                                params.get(1).map(|target| {
                                    BatchKind::ZncPlayback(Target::parse(
                                        target,
                                        self.chantypes(),
                                        self.statusmsg(),
                                        self.casemapping(),
                                    ))
                                })
                            }
                            _ => None,
                        };

                        self.batches.insert(reference, batch);
                    }
                    '-' => {
                        if let Some(mut finished) =
                            self.batches.remove(&reference)
                        {
                            // If nested multiline, assemble into a single
                            // message here as it needs to be done before
                            // extending into parent batch or finishing as an
                            // independent batch.
                            if let Some(BatchKind::Multiline(
                                tags,
                                user,
                                target,
                                Some(batch_kind),
                                text,
                            )) = &finished.kind
                            {
                                let encoded = multiline_encoded(
                                    user.as_ref(),
                                    *batch_kind,
                                    target,
                                    text,
                                    tags.clone(),
                                );

                                finished.events.extend(self.handle(
                                    encoded,
                                    finished.context.clone(),
                                    config,
                                )?);
                            }

                            if let Some(parent) = batch_tag
                                .as_ref()
                                .and_then(|batch| self.batches.get_mut(batch))
                            {
                                parent.events.extend(finished.events);

                                // Ergo treats a multiline message a single
                                // message toward the chathistory limit while
                                // soju treats it as the number of batch
                                // messages;  for our purposes the number of
                                // batch messages is the more conservative
                                // option.  This is expected to change in the
                                // future, for all chathistory implementation to
                                // treat a multiline message as a single
                                // message.
                                parent.size += finished.size;
                            } else {
                                match &finished.kind {
                                    Some(BatchKind::ChathistoryTarget(
                                        batch_target,
                                    )) => {
                                        let continuation_subcommand = self
                                            .finish_chathistory_request(
                                                Some(batch_target),
                                                finished.size,
                                                &finished.events,
                                                finished.chathistory_end,
                                            );

                                        self.clear_chathistory_request(Some(
                                            batch_target,
                                        ));

                                        if let Some(continuation_subcommand) =
                                            continuation_subcommand
                                        {
                                            self.send_chathistory_request(
                                                continuation_subcommand,
                                                TokenPriority::High,
                                            );
                                        }
                                    }
                                    Some(BatchKind::ChathistoryTargets) => {
                                        let continuation_subcommand = self
                                            .finish_chathistory_request(
                                                None,
                                                finished.size,
                                                &finished.events,
                                                finished.chathistory_end,
                                            );

                                        self.clear_chathistory_request(None);

                                        // Chathistory is automatically
                                        // requested when JOIN is received, so
                                        // we can filter out unprefixed channels
                                        // once continuation_subcommand has been
                                        // created.
                                        finished.events.retain(|event| match event {
                                            Event::ChatHistoryTargetReceived(target, _) => match target {
                                                Target::Channel(channel) => {
                                                    !channel.prefixes().is_empty()
                                                        && self.chanmap.contains_key(channel)
                                                }
                                                Target::Query(_) => true,
                                            },
                                            _ => true,
                                        });

                                        if let Some(continuation_subcommand) =
                                            continuation_subcommand
                                        {
                                            self.send_chathistory_request(
                                                continuation_subcommand,
                                                TokenPriority::High,
                                            );
                                        } else {
                                            finished.events.push(
                                                Event::ChatHistoryTargetsReceived(
                                                    Utc::now(),
                                                ),
                                            );
                                        }
                                    }
                                    Some(BatchKind::Multiline(
                                        _,
                                        _,
                                        _,
                                        _,
                                        _,
                                    ))
                                    | Some(BatchKind::ZncPlayback(_))
                                    | None => (),
                                };

                                return Ok(finished.events);
                            }
                        }
                    }
                    _ => {}
                }

                return Ok(vec![]);
            }
            _ if batch_tag.is_some() => {
                if let Some(batch_tag) = batch_tag.as_ref() {
                    let is_chathistory_context =
                        message.tags.contains_key("draft/chathistory-context");

                    let events = match self
                        .batches
                        .get(batch_tag)
                        .and_then(|batch| batch.kind.as_ref())
                    {
                        Some(BatchKind::ChathistoryTarget(batch_target)) => {
                            self.handle_chathistory(
                                message,
                                batch_target.clone(),
                            )
                        }
                        Some(BatchKind::Multiline(_, _, _, _, _)) => {
                            self.handle_multiline(message, batch_tag);
                            vec![]
                        }
                        Some(BatchKind::ChathistoryTargets) | None => {
                            self.handle(message, context, config)?
                        }
                        Some(BatchKind::ZncPlayback(batch_target)) => self
                            .handle_znc_playback(message, batch_target.clone()),
                    };

                    if let Some(batch) = self.batches.get_mut(batch_tag) {
                        if !is_chathistory_context {
                            batch.size += 1;
                        }

                        batch.events.extend(events);

                        return Ok(vec![]);
                    } else {
                        return Ok(events);
                    }
                }
            }
            _ if is_reaction(&message) => {
                return Ok(vec![Event::Reaction {
                    message,
                    our_nick: self.nickname().to_owned(),
                    deduplicate: false,
                    notification_enabled: self.notification_blackout.allowed(),
                    labeled_response_context: context.and_then(Into::into),
                }]);
            }
            _ if matches!(message.command, Command::REDACT(_, _, _)) => {
                return Ok(vec![Event::Redaction(
                    message,
                    self.nickname().to_owned(),
                )]);
            }
            // Reroute whois, whowas, mode, and invite responses
            Command::Numeric(
                RPL_WHOISCERTFP | RPL_WHOISREGNICK | RPL_WHOISUSER
                | RPL_WHOISSERVER | RPL_WHOISOPERATOR | RPL_WHOISIDLE
                | RPL_WHOISCHANNELS | RPL_WHOISSPECIAL | RPL_WHOISACCOUNT
                | RPL_WHOISACTUALLY | RPL_WHOISHOST | RPL_WHOISMODES
                | RPL_WHOISSECURE | RPL_WHOISBOT | RPL_AWAY | RPL_ENDOFWHOIS
                | RPL_WHOWASUSER | RPL_ENDOFWHOWAS | RPL_UMODEIS
                | ERR_NOSUCHNICK | ERR_NOSUCHSERVER | ERR_NONICKNAMEGIVEN
                | ERR_WASNOSUCHNICK | ERR_NEEDMOREPARAMS | ERR_USERSDONTMATCH
                | ERR_UMODEUNKNOWNFLAG | RPL_INVITING | ERR_NOSUCHCHANNEL
                | ERR_NOTONCHANNEL | ERR_CHANOPRIVSNEEDED | ERR_USERONCHANNEL,
                _,
            ) if self.reroute_responses_to.is_some() => {
                if let Some(target) =
                    self.reroute_responses_to.as_ref().map(Destination::from)
                {
                    return Ok(vec![Event::WithTarget {
                        message,
                        our_nick: self.nickname().to_owned(),
                        target,
                        deduplicate: false,
                    }]);
                }
            }
            Command::BOUNCER(subcommand, params) if subcommand == "NETWORK" => {
                if !self.is_primary() {
                    // we should only be receiving bouncer communication on the primary channel. Just for
                    // safety and future proofing, ignore in bound networks.
                    return Ok(vec![]);
                }
                let [netid, network] = params.as_slice() else {
                    bail!(
                        "[{}] Invalid BOUNCER NETWORKS message {:?}",
                        self.server,
                        &message.command,
                    );
                };

                if !self.sasl_succeeded {
                    // our connection isn't currently SASL. We have to assume that SASL won't
                    // succeed for any other bouncer networks, which means they won't be able to
                    // connect. Don't add them.
                    log::warn!(
                        "[{}] We are able to add bouncer networks, but cannot because the connection is not SASL",
                        self.server,
                    );
                    return Ok(vec![]);
                }

                let network = BouncerNetwork::parse(netid, network)?;
                let network_config = self.config.bouncer_config();
                return Ok(vec![Event::BouncerNetwork(
                    Server {
                        network: Some(network.into()),
                        ..self.server.clone()
                    },
                    network_config,
                )]);
            }
            Command::CAP(_, sub, a, b) if sub == "LS" => {
                let (caps, asterisk) = match (a, b) {
                    (Some(caps), None) => (caps, None),
                    (Some(asterisk), Some(caps)) => (caps, Some(asterisk)),
                    // Unreachable
                    (None, None) | (None, Some(_)) => return Ok(vec![]),
                };

                self.capabilities.extend_list(caps.split(' '));

                // Finished
                if asterisk.is_none() {
                    let requested =
                        self.capabilities.create_requested(&self.config);

                    if !requested.is_empty() {
                        // Request
                        self.registration_step = RegistrationStep::Req;

                        for message in group_capability_requests(&requested) {
                            self.handle.try_send(message)?;
                        }
                    } else {
                        // If none requested, end negotiation
                        self.registration_step = RegistrationStep::End;
                        self.handle.try_send(command!("CAP", "END"))?;
                    }
                }
            }
            Command::CAP(_, sub, a, b) if sub == "ACK" => {
                // TODO this code is duplicated several times. Fix in `Command`.
                let caps = ok!(b.as_ref().or(a.as_ref()));

                log::info!(
                    "[{}] capabilities acknowledged: {caps}",
                    self.server
                );

                self.capabilities
                    .acknowledge(caps.split(' ').map(String::from));

                if self.registration_step == RegistrationStep::Req {
                    if let Some(sasl) = self.config.sasl.as_ref().filter(|_| {
                        self.capabilities.acknowledged(Capability::Sasl)
                    }) {
                        self.registration_step = RegistrationStep::Sasl;
                        self.handle.try_send(command!(
                            "AUTHENTICATE",
                            sasl.command()
                        ))?;
                    } else {
                        self.registration_step = RegistrationStep::End;
                        self.handle.try_send(command!("CAP", "END"))?;
                    }
                }
            }
            Command::CAP(_, sub, a, b) if sub == "NAK" => {
                let caps = ok!(b.as_ref().or(a.as_ref()));

                log::warn!(
                    "[{}] capabilities not acknowledged: {caps}",
                    self.server
                );

                // End if we didn't move to sasl or already ended
                if self.registration_step < RegistrationStep::Sasl {
                    self.registration_step = RegistrationStep::End;
                    self.handle.try_send(command!("CAP", "END"))?;
                }
            }
            Command::CAP(_, sub, a, b) if sub == "NEW" => {
                let caps = ok!(b.as_ref().or(a.as_ref()));

                self.capabilities.extend_list(caps.split(' '));

                let requested =
                    self.capabilities.create_requested(&self.config);

                if !requested.is_empty() {
                    for message in group_capability_requests(&requested) {
                        self.handle.try_send(message)?;
                    }
                }
            }
            Command::CAP(_, sub, a, b) if sub == "DEL" => {
                let caps = ok!(b.as_ref().or(a.as_ref()));

                log::info!(
                    "[{}] capabilities no longer supported: {caps}",
                    self.server
                );

                self.capabilities.delete(caps.split(' ').map(String::from));
            }
            Command::AUTHENTICATE(param) if param == "+" => {
                if let Some(sasl) = self.config.sasl.as_ref() {
                    log::info!(
                        "[{}] sasl auth: {}",
                        self.server,
                        sasl.command()
                    );

                    for param in sasl.params(&self.config.nickname) {
                        self.handle
                            .try_send(command!("AUTHENTICATE", param))?;
                    }
                    // now that we are authenticated, we can connect to our desired network
                    if let Some(id) = self.server.bouncer_netid() {
                        self.handle
                            .try_send(command!("BOUNCER", "BIND", id))?;
                    }
                }
            }
            Command::Numeric(RPL_LISTSTART, _) => {
                self.channel_discovery_manager.status =
                    Some(channel_discovery::Status::Receiving(Utc::now()));
                return Ok(vec![]);
            }
            Command::Numeric(RPL_LIST, args) => {
                let channel = ok!(args.get(1)).clone();
                let user_count = ok!(args.get(2)).clone();
                let topic = ok!(args.get(3)).clone();

                self.channel_discovery_manager
                    .push(channel, topic, user_count);

                self.channel_discovery_manager.status =
                    Some(channel_discovery::Status::Receiving(Utc::now()));
                return Ok(vec![]);
            }
            Command::Numeric(RPL_LISTEND, _) => {
                self.channel_discovery_manager.status =
                    Some(channel_discovery::Status::Updated(Utc::now()));
                return Ok(vec![]);
            }
            Command::Numeric(RPL_LOGGEDIN, args) => {
                log::info!("[{}] logged in", self.server);

                self.logged_in = true;

                if !self.registration_required_channels.is_empty() {
                    for message in group_joins(
                        &self.registration_required_channels,
                        &self.config.channel_keys,
                        find_target_limit(&self.isupport, "JOIN"),
                    ) {
                        self.handle.try_send(message)?;
                    }

                    self.registration_required_channels.clear();
                }

                if !self.capabilities.acknowledged(Capability::AccountNotify) {
                    let accountname = ok!(args.get(2));

                    let old_user = User::from(self.nickname().to_owned());

                    self.chanmap.values_mut().for_each(|channel| {
                        if let Some(user) = channel.users.take(&old_user) {
                            channel
                                .users
                                .insert(user.with_accountname(accountname));
                        }
                    });
                }

                return Ok(vec![Event::LoggedIn(
                    chathistory_entry_server_time(message),
                )]);
            }
            Command::Numeric(RPL_LOGGEDOUT, _) => {
                log::info!("[{}] logged out", self.server);

                self.logged_in = false;

                if !self.capabilities.acknowledged(Capability::AccountNotify) {
                    let old_user = User::from(self.nickname().to_owned());

                    self.chanmap.values_mut().for_each(|channel| {
                        if let Some(user) = channel.users.take(&old_user) {
                            channel.users.insert(user.with_accountname("*"));
                        }
                    });
                }
            }
            Command::PRIVMSG(_, text) | Command::NOTICE(_, text) => {
                if let Some(user) = message.user(self.casemapping()) {
                    let is_echo = user.nickname() == self.nickname();

                    if is_echo
                        && self
                            .capabilities
                            .acknowledged(Capability::EchoMessage)
                    {
                        if let Some(username) = user.username() {
                            self.resolved_user = Some(username.to_string());
                        }
                        if let Some(hostname) = user.hostname() {
                            self.resolved_host = Some(hostname.to_string());
                        }
                    }

                    let dcc_command = dcc::decode(text);
                    let ctcp_query = ctcp::parse_query(text);

                    // DCC Handling
                    if let Some(command) = dcc_command {
                        // Ignore echoed DCC messages
                        if is_echo {
                            return Ok(vec![]);
                        }

                        match command {
                            dcc::Command::Send(request) => {
                                log::trace!(
                                    "[{}] DCC Send => {request:?}",
                                    self.server
                                );
                                return Ok(vec![Event::FileTransferRequest(
                                    file_transfer::ReceiveRequest {
                                        from: user,
                                        dcc_send: request,
                                        server: self.server.clone(),
                                        server_handle: self.handle.clone(),
                                    },
                                )]);
                            }
                            dcc::Command::Unsupported(command) => {
                                bail!("Unsupported DCC command: {command}",);
                            }
                        }
                    };

                    // CTCP Handling
                    if let Some(query) = ctcp_query {
                        let is_action = message::is_action(text);

                        // Ignore CTCP Action queries.
                        if !is_action && !is_echo {
                            // Response to us sending a CTCP request to another client
                            if matches!(&message.command, Command::NOTICE(_, _))
                            {
                                let event = Event::PrivOrNotice {
                                    message,
                                    our_nick: self.nickname().to_owned(),
                                    notification_enabled: self
                                        .notification_blackout
                                        .allowed(),
                                    deduplicate: false,
                                    labeled_response_context: None,
                                };

                                return Ok(vec![event]);
                            }

                            // Response to a client sending us a CTCP request
                            if matches!(
                                &message.command,
                                Command::PRIVMSG(_, _)
                            ) {
                                match query.command {
                                    ctcp::Command::Action => (),
                                    ctcp::Command::ClientInfo => {
                                        self.send(
                                            None,
                                            ctcp::response_message(
                                                &query.command,
                                                user.nickname().to_string(),
                                                Some(config.ctcp.client_info()),
                                            )
                                            .into(),
                                            TokenPriority::High,
                                        );
                                    }
                                    ctcp::Command::UserInfo => {
                                        if config.ctcp.userinfo.is_some() {
                                            self.send(
                                                None,
                                                ctcp::response_message(
                                                    &query.command,
                                                    user.nickname().to_string(),
                                                    config
                                                        .ctcp
                                                        .userinfo
                                                        .clone(),
                                                )
                                                .into(),
                                                TokenPriority::High,
                                            );
                                        }
                                    }
                                    ctcp::Command::DCC => (),
                                    ctcp::Command::Ping => {
                                        if config.ctcp.ping {
                                            self.send(
                                                None,
                                                ctcp::response_message(
                                                    &query.command,
                                                    user.nickname().to_string(),
                                                    query.params,
                                                )
                                                .into(),
                                                TokenPriority::High,
                                            );
                                        }
                                    }
                                    ctcp::Command::Source => {
                                        if config.ctcp.source {
                                            self.send(
                                                None,
                                                ctcp::response_message(
                                                    &query.command,
                                                    user.nickname().to_string(),
                                                    Some(SOURCE_WEBSITE),
                                                )
                                                .into(),
                                                TokenPriority::High,
                                            );
                                        }
                                    }
                                    ctcp::Command::Version => {
                                        if config.ctcp.version {
                                            self.send(
                                                None,
                                                ctcp::response_message(
                                                    &query.command,
                                                    user.nickname().to_string(),
                                                    Some(format!(
                                                        "Halloy {VERSION}"
                                                    )),
                                                )
                                                .into(),
                                                TokenPriority::High,
                                            );
                                        }
                                    }
                                    ctcp::Command::Time => {
                                        if config.ctcp.time {
                                            let utc_time = Utc::now();
                                            let formatted = utc_time
                                                .to_rfc3339_opts(
                                                chrono::SecondsFormat::Millis,
                                                true,
                                            );

                                            self.send(
                                                None,
                                                ctcp::response_message(
                                                    &query.command,
                                                    user.nickname().to_string(),
                                                    Some(formatted),
                                                )
                                                .into(),
                                                TokenPriority::High,
                                            );
                                        }
                                    }
                                    ctcp::Command::Unknown(command) => {
                                        log::debug!(
                                            "[{}] Ignoring CTCP command {command}: Unknown command",
                                            self.server
                                        );
                                    }
                                }
                            }

                            return Ok(vec![]);
                        }
                    }

                    if self.isupport.contains_key(&isupport::Kind::BOT)
                        && let Some(channel) =
                            self.message_channel_target(&message.command)
                        && let Some(ch) = self.chanmap.get_mut(&channel)
                    {
                        ch.users.update_user(
                            &user,
                            None,
                            Some(message.tags.contains_key("bot")),
                        );
                    }

                    let user_query = target::Query::from(&user);
                    let direct_message = self
                        .message_query_target(&message.command)
                        .as_ref()
                        .is_some_and(|query| {
                            query.as_normalized_str()
                                == self.nickname().as_normalized_str()
                        });

                    if let Some(channel) =
                        self.message_channel_target(&message.command)
                    {
                        self.clear_channel_typing(&channel, user.nickname());
                    } else if direct_message {
                        self.clear_query_typing(&user_query);
                    }

                    if direct_message {
                        self.record_query(&user_query);
                    }

                    let event = Event::PrivOrNotice {
                        message: message.clone(),
                        our_nick: self.nickname().to_owned(),
                        notification_enabled: self
                            .notification_blackout
                            .allowed(),
                        deduplicate: false,
                        labeled_response_context: context.and_then(Into::into),
                    };

                    // Event::DirectMessage is currently only used to send a
                    // notification, so only return the event it notifications
                    // are allowed.
                    if direct_message && self.notification_blackout.allowed() {
                        return Ok(vec![
                            event,
                            Event::DirectMessage(
                                message,
                                self.nickname().to_owned(),
                                user,
                            ),
                        ]);
                    } else {
                        return Ok(vec![event]);
                    }
                }
            }
            Command::INVITE(invitee, _) => {
                let inviter = ok!(message.user(self.casemapping()));

                if inviter.nickname() == self.nickname() {
                    // Ignore since we should receive a RPL_INVITING for
                    // invites sent by the user
                    return Ok(vec![]);
                }

                let invitee = Nick::from_str(invitee, self.casemapping());

                let event = Event::Single {
                    message: message.clone(),
                    our_nick: self.nickname().to_owned(),
                    deduplicate: false,
                };

                if invitee.as_nickref() == self.nickname() {
                    return Ok(vec![
                        event,
                        Event::DirectMessage(
                            message,
                            self.nickname().to_owned(),
                            inviter,
                        ),
                    ]);
                } else {
                    return Ok(vec![event]);
                }
            }
            Command::NICK(nick) => {
                let old_user = ok!(message.user(self.casemapping()));
                let ourself = self.nickname() == old_user.nickname();

                if ourself {
                    self.resolved_nick = Some(Nick::from_string(
                        nick.to_string(),
                        self.casemapping(),
                    ));
                }

                let channels = self.user_channels(old_user.nickname());

                let new_nick =
                    Nick::from_str(nick.as_str(), self.casemapping());

                self.chanmap.values_mut().for_each(|channel| {
                    if let Some(user) = channel.users.take(&old_user) {
                        channel
                            .users
                            .insert(user.with_nickname(new_nick.clone()));
                    }
                });

                let (server_time, received_with_server_time) =
                    message.server_time_or_now();

                return Ok(vec![Event::Broadcast(Broadcast::Nickname {
                    old_user,
                    new_nick,
                    ourself,
                    channels,
                    server_time,
                    received_with_server_time,
                })]);
            }
            Command::Numeric(ERR_NICKNAMEINUSE | ERR_ERRONEUSNICKNAME, _)
                if self.resolved_nick.is_none() =>
            {
                // Try alt nicks
                match &mut self.alt_nick {
                    Some(index) => {
                        if *index == self.config.alt_nicks.len() - 1 {
                            self.alt_nick = None;
                        } else {
                            *index += 1;
                        }
                    }
                    None if !self.config.alt_nicks.is_empty() => {
                        self.alt_nick = Some(0);
                    }
                    None => {}
                }

                if let Some(nick) =
                    self.alt_nick.and_then(|i| self.config.alt_nicks.get(i))
                {
                    self.handle.try_send(command!("NICK", nick))?;
                }
            }
            Command::Numeric(RPL_WELCOME, args) => {
                // Updated actual nick
                let nick = ok!(args.first());
                self.resolved_nick = Some(Nick::from_string(
                    nick.to_string(),
                    self.casemapping(),
                ));
            }
            Command::Numeric(RPL_MYINFO, args) => {
                let server_version = ok!(args.get(2));

                self.features.enable_supported(server_version);
            }
            // QUIT
            Command::QUIT(comment) => {
                let user = ok!(message.user(self.casemapping()));

                let channels = self.user_channels(user.nickname());

                self.chanmap.values_mut().for_each(|channel| {
                    channel.users.remove(&user);
                });

                let (server_time, received_with_server_time) =
                    message.server_time_or_now();

                return Ok(vec![Event::Broadcast(Broadcast::Quit {
                    user,
                    comment: comment.clone(),
                    channels,
                    server_time,
                    received_with_server_time,
                })]);
            }
            Command::PART(channel, _) => {
                let user = ok!(message.user(self.casemapping()));

                if user.nickname() == self.nickname() {
                    self.chanmap.shift_remove(&context!(
                        target::Channel::parse(
                            channel,
                            self.chantypes(),
                            self.statusmsg(),
                            self.casemapping(),
                        )
                    ));
                } else if let Some(channel) =
                    self.chanmap.get_mut(&context!(target::Channel::parse(
                        channel,
                        self.chantypes(),
                        self.statusmsg(),
                        self.casemapping(),
                    )))
                {
                    channel.users.remove(&user);
                }
            }
            Command::JOIN(channel, accountname) => {
                let casemapping = self.casemapping();

                let user = ok!(message.user(casemapping));

                let target_channel = context!(target::Channel::parse(
                    channel,
                    self.chantypes(),
                    self.statusmsg(),
                    casemapping,
                ));

                if user.nickname() == self.nickname() {
                    let casemapping = self.casemapping();
                    let chantypes = self.chantypes().to_vec();
                    let _ = self.chanmap.insert_sorted_by(
                        target_channel.clone(),
                        Channel::default(),
                        |c1, _, c2, _| {
                            compare_channels(
                                &self.config,
                                &chantypes,
                                casemapping,
                                c1.as_normalized_str(),
                                c2.as_normalized_str(),
                            )
                        },
                    );

                    // Add channel to WHO poll queue
                    if !self
                        .who_polls
                        .iter()
                        .any(|who_poll| who_poll.channel == target_channel)
                    {
                        self.who_polls.push_back(WhoPoll {
                            channel: target_channel.clone(),
                            status: WhoStatus::Joined,
                        });
                    }

                    if !self.mode_requests.iter().any(|mode_request| {
                        mode_request.channel == target_channel
                    }) {
                        self.mode_requests.push(ModeRequest {
                            channel: target_channel.clone(),
                            status: ModeStatus::Joined(Instant::now()),
                        });
                    }

                    return Ok(vec![Event::JoinedChannel(
                        target_channel,
                        chathistory_entry_server_time(message),
                    )]);
                } else if let Some(channel) =
                    self.chanmap.get_mut(&target_channel)
                {
                    let mut user = if self
                        .capabilities
                        .acknowledged(Capability::ExtendedJoin)
                    {
                        accountname
                            .as_ref()
                            .map_or(user.clone(), |accountname| {
                                user.with_accountname(accountname)
                            })
                    } else {
                        user
                    };

                    if message.tags.contains_key("bot") {
                        user.update_bot(true);
                    }

                    channel.users.insert(user);
                }
            }
            Command::KICK(channel, victim, reason) => {
                let casemapping = self.casemapping();

                if let Ok(channel) = target::Channel::parse(
                    channel,
                    self.chantypes(),
                    self.statusmsg(),
                    casemapping,
                ) {
                    if casemapping.normalize(victim)
                        == self.nickname().as_normalized_str()
                    {
                        self.chanmap.shift_remove(&channel);

                        let (server_time, received_with_server_time) =
                            message.server_time_or_now();

                        return Ok(vec![
                            Event::Broadcast(Broadcast::Kick {
                                kicker: ok!(message.user(casemapping)),
                                victim: User::from(self.nickname().to_owned()),
                                reason: reason.clone(),
                                channel,
                                server_time,
                                received_with_server_time,
                            }),
                            Event::Single {
                                message,
                                our_nick: self.nickname().to_owned(),
                                deduplicate: false,
                            },
                        ]);
                    } else if let Some(channel) = self.chanmap.get_mut(&channel)
                    {
                        channel.users.remove(&User::from(Nick::from_str(
                            victim.as_str(),
                            casemapping,
                        )));
                    }
                }
            }
            Command::Numeric(RPL_WHOREPLY, args) => {
                let channel = ok!(args.get(1));

                let casemapping = self.casemapping();

                if let Ok(target_channel) = target::Channel::parse(
                    channel,
                    self.chantypes(),
                    self.statusmsg(),
                    casemapping,
                ) {
                    let user_request = self.user_who_request(&target_channel);

                    if let Some(client_channel) =
                        self.chanmap.get_mut(&target_channel)
                    {
                        client_channel.update_user_status(
                            ok!(args.get(5)),
                            ok!(args.get(6)),
                            casemapping,
                            isupport::get_bot_mode_char(&self.isupport),
                        );

                        if let Some(who_poll) = self
                            .who_polls
                            .iter_mut()
                            .find(|who_poll| who_poll.channel == target_channel)
                            && let WhoStatus::Requested(source, _, None) =
                                &who_poll.status
                        {
                            who_poll.status =
                                WhoStatus::Receiving(source.clone(), None);
                            log::trace!(
                                "[{}] {channel} - WHO receiving...",
                                self.server
                            );
                        }
                    }

                    if !user_request {
                        // User did not request, don't save to history
                        return Ok(vec![]);
                    // Reroute who responses
                    } else if let Some(target) = self
                        .reroute_responses_to
                        .as_ref()
                        .map(Destination::from)
                    {
                        return Ok(vec![Event::WithTarget {
                            message,
                            our_nick: self.nickname().to_owned(),
                            target,
                            deduplicate: false,
                        }]);
                    }
                }
            }
            Command::Numeric(RPL_WHOSPCRPL, args) => {
                let channel = ok!(args.get(2));

                let casemapping = self.casemapping();

                if let Ok(target_channel) = target::Channel::parse(
                    channel,
                    self.chantypes(),
                    self.statusmsg(),
                    casemapping,
                ) {
                    let user_request = self.user_who_request(&target_channel);

                    if let Some(client_channel) =
                        self.chanmap.get_mut(&target_channel)
                        && let Some(who_poll) = self
                            .who_polls
                            .iter_mut()
                            .find(|who_poll| who_poll.channel == target_channel)
                    {
                        match &who_poll.status {
                            WhoStatus::Requested(
                                source,
                                _,
                                Some(request_token),
                            ) if matches!(source, WhoSource::Poll) => {
                                if let Ok(token) =
                                    ok!(args.get(1)).parse::<WhoToken>()
                                    && *request_token == token
                                {
                                    who_poll.status = WhoStatus::Receiving(
                                        source.clone(),
                                        Some(*request_token),
                                    );
                                    log::trace!(
                                        "[{}] {channel} - WHO receiving...",
                                        self.server
                                    );
                                }
                            }
                            WhoStatus::Requested(
                                WhoSource::User,
                                _,
                                Some(request_token),
                            ) => {
                                who_poll.status = WhoStatus::Receiving(
                                    WhoSource::User,
                                    Some(*request_token),
                                );

                                log::trace!(
                                    "[{}] {channel} - WHO receiving...",
                                    self.server
                                );
                            }
                            _ => (),
                        }

                        if let WhoStatus::Receiving(WhoSource::Poll, Some(_)) =
                            &who_poll.status
                        {
                            // Check token to ~ensure reply is to poll request
                            if let Ok(token) =
                                ok!(args.get(1)).parse::<WhoToken>()
                            {
                                if token == WhoXPollParameters::Default.token()
                                {
                                    client_channel.update_user_status(
                                        ok!(args.get(3)),
                                        ok!(args.get(4)),
                                        casemapping,
                                        isupport::get_bot_mode_char(
                                            &self.isupport,
                                        ),
                                    );
                                } else if token
                                    == WhoXPollParameters::WithAccountName
                                        .token()
                                {
                                    let user = ok!(args.get(3));

                                    client_channel.update_user_status(
                                        user,
                                        ok!(args.get(4)),
                                        casemapping,
                                        isupport::get_bot_mode_char(
                                            &self.isupport,
                                        ),
                                    );

                                    client_channel.update_user_accountname(
                                        user,
                                        ok!(args.get(5)),
                                        casemapping,
                                    );
                                }
                            }
                        }
                    }

                    if !user_request {
                        // User did not request, don't save to history
                        return Ok(vec![]);
                    // Reroute who responses
                    } else if let Some(target) = self
                        .reroute_responses_to
                        .as_ref()
                        .map(Destination::from)
                    {
                        return Ok(vec![Event::WithTarget {
                            message,
                            our_nick: self.nickname().to_owned(),
                            target,
                            deduplicate: false,
                        }]);
                    }
                }
            }
            Command::Numeric(RPL_ENDOFWHO, args) => {
                let mask = ok!(args.get(1));

                if let Ok(target_channel) = target::Channel::parse(
                    mask,
                    self.chantypes(),
                    self.statusmsg(),
                    self.casemapping(),
                ) {
                    let user_request = self.user_who_request(&target_channel);

                    if let Some(pos) = self
                        .who_polls
                        .iter()
                        .position(|who_poll| who_poll.channel == target_channel)
                    {
                        self.who_polls[pos].status = WhoStatus::Received;

                        if let Some(who_poll) = self.who_polls.remove(pos)
                            && self.chanmap.contains_key(&target_channel)
                            && self.config.who_poll_enabled
                        {
                            self.who_polls.push_back(who_poll);
                        }

                        // Prioritize WHO requests due to joining a channel
                        if let Some(pos) = self
                            .who_polls
                            .iter()
                            .position(|who_poll| {
                                matches!(who_poll.status, WhoStatus::Joined)
                            })
                            .or(self.who_polls.iter().position(|who_poll| {
                                matches!(who_poll.status, WhoStatus::Received)
                            }))
                        {
                            self.who_polls[pos].status =
                                WhoStatus::Waiting(Instant::now());

                            if pos != 0
                                && let Some(who_poll) =
                                    self.who_polls.remove(pos)
                            {
                                self.who_polls.push_front(who_poll);
                            }
                        }
                    }

                    log::trace!("[{}] {mask} - WHO done", self.server);

                    if let Some(client_channel) =
                        self.chanmap.get_mut(&target_channel)
                    {
                        client_channel.who_init = true;
                    }

                    if !user_request {
                        self.who_poll_interval.long_enough();

                        // User did not request, don't save to history
                        return Ok(vec![]);
                    // Reroute who responses
                    } else if let Some(target) = self
                        .reroute_responses_to
                        .as_ref()
                        .map(Destination::from)
                    {
                        return Ok(vec![Event::WithTarget {
                            message,
                            our_nick: self.nickname().to_owned(),
                            target,
                            deduplicate: false,
                        }]);
                    }
                } else if mask == "*" {
                    // Some servers respond with the mask * instead of the requested
                    // channel name when rate-limiting WHO requests
                    let target_channel = target::Channel::from_str(
                        mask,
                        self.chantypes(),
                        self.casemapping(),
                    );

                    if let Some(pos) = self
                        .who_polls
                        .iter()
                        .position(|who_poll| who_poll.channel == target_channel)
                    {
                        self.who_polls.remove(pos);
                    } else {
                        // User did not request, treat as part of rate-limiting response
                        // (in conjunction with RPL_TRYAGAIN) and don't save to history.
                        if let Some(who_poll) = self.who_polls.front_mut() {
                            who_poll.status =
                                WhoStatus::Waiting(Instant::now());
                        }

                        self.who_polls.iter_mut().skip(1).for_each(
                            |who_poll| who_poll.status = WhoStatus::Received,
                        );

                        return Ok(vec![]);
                    }
                }
            }
            Command::AWAY(args) => {
                let away = args.is_some();
                let user = ok!(message.user(self.casemapping()));

                for channel in self.chanmap.values_mut() {
                    if let Some(mut user) = channel.users.take(&user) {
                        user.update_away(away);
                        channel.users.insert(user);
                    }
                }
            }
            // RPL_UNAWAY is a reply to "/AWAY" from the server
            // for the client/user itself.
            Command::Numeric(RPL_UNAWAY, _) => {
                let user = User::from(self.nickname().to_owned());

                for channel in self.chanmap.values_mut() {
                    if let Some(mut user) = channel.users.take(&user) {
                        user.update_away(false);
                        channel.users.insert(user);
                    }
                }
            }
            // RPL_UNAWAY is a reply to "/AWAY <msg>" from the server
            // for the client/user itself.
            Command::Numeric(RPL_NOWAWAY, _) => {
                let user = User::from(self.nickname().to_owned());

                for channel in self.chanmap.values_mut() {
                    if let Some(mut user) = channel.users.take(&user) {
                        user.update_away(true);
                        channel.users.insert(user);
                    }
                }
            }
            Command::MODE(target, Some(modes), Some(args)) => {
                let casemapping = self.casemapping();

                if let Ok(channel) = target::Channel::parse(
                    target,
                    self.chantypes(),
                    self.statusmsg(),
                    casemapping,
                ) {
                    let modes = mode::parse::<mode::Channel>(
                        modes,
                        args,
                        self.chanmodes(),
                        self.prefix(),
                    );
                    let target_channel = channel.clone();

                    if let Some(channel) = self.chanmap.get_mut(&channel) {
                        let mut channel_mode_changed = false;

                        for mode in modes {
                            if let Some((op, lookup)) =
                                mode.operation().zip(mode.arg().map(|nick| {
                                    User::from(Nick::from_str(
                                        nick,
                                        casemapping,
                                    ))
                                }))
                                && let Some(mut user) =
                                    channel.users.take(&lookup)
                            {
                                user.update_access_level(op, *mode.value());
                                channel.users.insert(user);
                            } else {
                                channel_mode_changed = true;
                            }
                        }

                        // Request MODE to update the channel.
                        if channel_mode_changed {
                            self.send(
                                None,
                                command!("MODE", target_channel.to_string())
                                    .into(),
                                TokenPriority::Low,
                            );
                        }
                    }
                } else {
                    let modes = mode::parse::<mode::User>(
                        modes,
                        args,
                        self.chanmodes(),
                        self.prefix(),
                    );

                    let bot_change = modes.iter().find_map(|mode| match mode {
                        mode::Mode::Add(mode::User::Bot, _) => Some(true),
                        mode::Mode::Remove(mode::User::Bot, _) => Some(false),
                        _ => None,
                    });

                    if let Some(is_bot) = bot_change {
                        let lookup =
                            User::from(Nick::from_str(target, casemapping));
                        self.chanmap.values_mut().for_each(|channel| {
                            if let Some(mut user) = channel.users.take(&lookup)
                            {
                                user.update_bot(is_bot);
                                channel.users.insert(user);
                            }
                        });
                    }

                    // Only check for being logged in via mode if account-notify is not available,
                    // since it is not standardized across networks.

                    if casemapping.normalize(target)
                        == self.nickname().as_normalized_str()
                        && !self
                            .capabilities
                            .acknowledged(Capability::AccountNotify)
                        && !self.registration_required_channels.is_empty()
                        && modes.into_iter().any(|mode| {
                            matches!(
                                mode,
                                mode::Mode::Add(mode::User::Registered, None)
                            )
                        })
                    {
                        for message in group_joins(
                            &self.registration_required_channels,
                            &self.config.channel_keys,
                            find_target_limit(&self.isupport, "JOIN"),
                        ) {
                            self.handle.try_send(message)?;
                        }

                        self.registration_required_channels.clear();
                    }
                }
            }
            Command::Numeric(RPL_NAMREPLY, args) if args.len() > 3 => {
                let channel = ok!(args.get(2));

                let our_nick = self.nickname().to_owned();

                if let Some(channel) =
                    self.chanmap.get_mut(&context!(target::Channel::parse(
                        channel,
                        self.chantypes(),
                        self.statusmsg(),
                        self.casemapping(),
                    )))
                {
                    let mut our_user = None;
                    let mut our_host = None;

                    let casemapping =
                        isupport::get_casemapping_or_default(&self.isupport);
                    let prefix = isupport::get_prefix(&self.isupport);
                    for user in args[3].split(' ') {
                        if let Ok(user) = User::parse(user, casemapping, prefix)
                        {
                            if user.nickname() == our_nick
                                && self
                                    .capabilities
                                    .acknowledged(Capability::UserhostInNames)
                            {
                                if let Some(username) = user.username() {
                                    our_user = Some(username.to_string());
                                }
                                if let Some(hostname) = user.hostname() {
                                    our_host = Some(hostname.to_string());
                                }
                            }

                            channel.users.insert(user);
                        }
                    }

                    if our_user.is_some() {
                        self.resolved_user = our_user;
                    }
                    if our_host.is_some() {
                        self.resolved_host = our_host;
                    }

                    // Don't save to history if names list was triggered by JOIN
                    if !channel.names_init {
                        return Ok(vec![]);
                    }
                }
            }
            Command::Numeric(RPL_ENDOFNAMES, args) => {
                let target = ok!(args.get(1));

                let target = context!(target::Channel::parse(
                    target,
                    self.chantypes(),
                    self.statusmsg(),
                    self.casemapping(),
                ));

                if let Some(channel) = self.chanmap.get_mut(&target)
                    && !channel.names_init
                {
                    channel.names_init = true;

                    if let Some((subcommand, priority)) = self
                        .pending_chathistory_requests
                        .remove(&Target::Channel(target))
                    {
                        self.send_chathistory_request(subcommand, priority);
                    }

                    return Ok(vec![]);
                }
            }
            Command::TOPIC(channel, topic) => {
                let casemapping = self.casemapping();

                if let Some(channel) =
                    self.chanmap.get_mut(&context!(target::Channel::parse(
                        channel,
                        self.chantypes(),
                        self.statusmsg(),
                        casemapping,
                    )))
                {
                    if let Some(text) = topic
                        && !text.is_empty()
                    {
                        channel.topic.content =
                            Some(message::parse_fragments(text.clone()));
                        channel.topic.who = message.user(casemapping);
                        channel.topic.time =
                            Some(message.server_time_or_now().0);
                    } else {
                        channel.topic.content = None;
                        channel.topic.who = None;
                        channel.topic.time = None;
                    }
                }
            }
            Command::Numeric(RPL_TOPIC, args) => {
                let channel = ok!(args.get(1));

                if let Some(channel) =
                    self.chanmap.get_mut(&context!(target::Channel::parse(
                        channel,
                        self.chantypes(),
                        self.statusmsg(),
                        self.casemapping(),
                    )))
                {
                    let is_join_topic = channel.topic.content.is_none();

                    channel.topic.content = Some(message::parse_fragments(
                        ok!(args.get(2)).to_owned(),
                    ));

                    if is_join_topic {
                        if config
                            .buffer
                            .server_messages
                            .should_drop(Some(source::server::Kind::JoinTopic))
                        {
                            return Ok(vec![]);
                        } else {
                            // Prefix ':' to ensure it cannot match any valid
                            // message tag
                            message.tags.insert(
                                ":join_topic".to_string(),
                                String::new(),
                            );
                        }
                    }
                }
            }
            Command::Numeric(RPL_TOPICWHOTIME, args) => {
                let channel = ok!(args.get(1));

                if let Some(channel) =
                    self.chanmap.get_mut(&context!(target::Channel::parse(
                        channel,
                        self.chantypes(),
                        self.statusmsg(),
                        self.casemapping(),
                    )))
                {
                    let is_join_topic = channel.topic.who.is_none()
                        || channel.topic.time.is_none();

                    channel.topic.who = Some(context!(User::parse(
                        ok!(args.get(2)),
                        isupport::get_casemapping_or_default(&self.isupport),
                        isupport::get_prefix(&self.isupport),
                    )));

                    let timestamp =
                        Posix::from_seconds(ok!(args.get(3)).parse::<u64>()?);
                    channel.topic.time =
                        Some(timestamp.datetime().ok_or_else(|| {
                            anyhow!("Unable to parse timestamp: {timestamp:?}")
                        })?);

                    if is_join_topic {
                        if config
                            .buffer
                            .server_messages
                            .should_drop(Some(source::server::Kind::JoinTopic))
                        {
                            return Ok(vec![]);
                        } else {
                            // Prefix ':' to ensure it cannot match any valid
                            // message tag
                            message.tags.insert(
                                ":join_topic".to_string(),
                                String::new(),
                            );
                        }
                    }
                }
            }
            Command::Numeric(RPL_NOTOPIC, args) => {
                let channel = ok!(args.get(1));

                if let Some(channel) =
                    self.chanmap.get_mut(&context!(target::Channel::parse(
                        channel,
                        self.chantypes(),
                        self.statusmsg(),
                        self.casemapping(),
                    )))
                {
                    channel.topic.content = None;
                    channel.topic.who = None;
                    channel.topic.time = None;
                }
            }
            Command::Numeric(RPL_CHANNELMODEIS, args) => {
                let channel = ok!(args.get(1));

                if let Ok(target_channel) = target::Channel::parse(
                    channel,
                    self.chantypes(),
                    self.statusmsg(),
                    self.casemapping(),
                ) && let Some(channel) =
                    self.chanmap.get_mut(&target_channel)
                {
                    let mode_request_response =
                        if let Some((instant, mode_request)) = self
                            .mode_requests
                            .iter_mut()
                            .find_map(|mode_request| {
                                if mode_request.channel == target_channel
                                    && let ModeStatus::Joined(instant)
                                    | ModeStatus::Requested(instant) =
                                        mode_request.status
                                {
                                    Some((instant, mode_request))
                                } else {
                                    None
                                }
                            })
                        {
                            mode_request.status = ModeStatus::Received(instant);
                            true
                        } else {
                            false
                        };

                    channel.mode = args.get(2).cloned();

                    if mode_request_response {
                        return Ok(vec![]);
                    }
                }
            }
            Command::Numeric(RPL_CREATIONTIME, args) => {
                let channel = ok!(args.get(1));

                if let Ok(target_channel) = target::Channel::parse(
                    channel,
                    self.chantypes(),
                    self.statusmsg(),
                    self.casemapping(),
                ) && let Some(position) =
                    self.mode_requests.iter().position(|mode_request| {
                        mode_request.channel == target_channel
                            && matches!(
                                mode_request.status,
                                ModeStatus::Received(_)
                            )
                    })
                {
                    self.mode_requests.swap_remove(position);
                }

                // Do not save to buffer as the message is not readily
                // human-readable and comes bundled with any channel
                // mode change.
                return Ok(vec![]);
            }
            Command::Numeric(ERR_NOCHANMODES, args) => {
                let channel = context!(target::Channel::parse(
                    ok!(args.get(1)),
                    self.chantypes(),
                    self.statusmsg(),
                    self.casemapping(),
                ));

                // If the channel has not been joined but is in the configured channels,
                // then interpret this numeric as ERR_NEEDREGGEDNICK (which has the
                // same number as ERR_NOCHANMODES)
                if !self.chanmap.contains_key(&channel)
                    && self.config.channels.iter().any(|config_channel| {
                        config_channel == channel.as_str()
                    })
                {
                    self.registration_required_channels.push(channel.clone());
                }
            }
            Command::Numeric(RPL_ISUPPORT, args) => {
                let args_len = args.len();
                let mut events: Vec<Event> = Vec::new();
                for (index, arg) in args.iter().enumerate().skip(1) {
                    let operation = arg.parse::<isupport::Operation>();

                    match operation {
                        Ok(operation) => {
                            match operation {
                                isupport::Operation::Add(parameter) => {
                                    if let Some(kind) = parameter.kind() {
                                        log::info!(
                                            "[{}] adding ISUPPORT parameter: {:?}",
                                            self.server,
                                            parameter
                                        );

                                        self.isupport.insert(
                                            kind.clone(),
                                            parameter.clone(),
                                        );

                                        match parameter {
                                            isupport::Parameter::CASEMAPPING(casemapping) => {
                                                if let Some(resolved_nick) = self.resolved_nick.as_mut() {
                                                    resolved_nick.renormalize(casemapping);
                                                }
                                                self.configured_nick.renormalize(casemapping);

                                                // TODO: When casemapping
                                                // changes, ChannelUsers, etc should be
                                                // renormalized and resorted

                                                let chantypes = self.chantypes().to_vec();
                                                self.chanmap = take(&mut self.chanmap)
                                                    .into_iter()
                                                    .map(|(mut channel, data)| {
                                                        channel.renormalize(casemapping);
                                                        (channel, data)
                                                    })
                                                    .sorted_by(|(c1, _), (c2, _)| {
                                                        compare_channels(
                                                            &self.config,
                                                            &chantypes,
                                                            casemapping,
                                                            c1.as_normalized_str(),
                                                            c2.as_normalized_str(),
                                                        )
                                                    })
                                                    .collect();

                                                self.querymap = take(&mut self.querymap)
                                                    .into_iter()
                                                    .map(|(mut query, state)| {
                                                        query.renormalize(casemapping);
                                                        (query, state)
                                                    })
                                                    .collect();
                                            }
                                            isupport::Parameter::SAFERATE => {
                                                if let Some(ref mut anti_flood) = self.anti_flood {
                                                    for message in anti_flood.drain_tokens() {
                                                        if let Err(e) = self.handle.try_send(message.into()) {
                                                            log::warn!("[{}] Error sending message: {e}", self.server);
                                                        }
                                                    }
                                                }

                                                self.anti_flood = None;

                                                self.who_poll_interval.set_min(self.config.who_poll_interval);
                                            }
                                            isupport::Parameter::BOUNCER_NETID(ref id) => {
                                                match self.server.bouncer_netid() {
                                                    Some(requested_id) if id != requested_id => {
                                                        log::warn!(
                                                            "[{}] Requested bouncer id `{requested_id}`, but was connected to bouncer `{id}`",
                                                            self.server,
                                                        );
                                                        // quit on fatal error?
                                                        self.quit(None);
                                                    },
                                                    _ => (),
                                                }
                                                self.resolved_netid = Some(id.clone());
                                            }
                                            _ => (),
                                        }
                                        events.push(Event::AddedIsupportParam(
                                            parameter,
                                        ));
                                    } else {
                                        log::info!(
                                            "[{}] ignoring ISUPPORT parameter: {:?}",
                                            self.server,
                                            parameter
                                        );
                                    }
                                }
                                isupport::Operation::Remove(_) => {
                                    if let Some(kind) = operation.kind() {
                                        log::info!(
                                            "[{}] removing ISUPPORT parameter: {:?}",
                                            self.server,
                                            kind
                                        );
                                        self.isupport.remove(&kind);
                                    }
                                }
                            };
                        }
                        Err(error) => {
                            // Last argument is :are supported by this server
                            if index != args_len - 1 {
                                if error == isupport::UNKNOWN_ISUPPORT_PARAMETER
                                {
                                    log::info!(
                                        "[{}] unable to parse ISUPPORT parameter: {} ({})",
                                        self.server,
                                        arg,
                                        error
                                    );
                                } else {
                                    log::warn!(
                                        "[{}] unable to parse ISUPPORT parameter: {} ({})",
                                        self.server,
                                        arg,
                                        error
                                    );
                                }
                            }
                        }
                    }
                }

                return Ok(events);
            }
            Command::TAGMSG(_) => {
                if let Some(sender) = message.user(self.casemapping()) {
                    if sender.nickname() == self.nickname() {
                        return Ok(vec![]);
                    }

                    let typing =
                        message.tags.get("+typing").map(String::as_str);

                    if let Some(channel) =
                        self.message_channel_target(&message.command)
                    {
                        self.handle_channel_typing_tagmsg(
                            &channel,
                            sender.nickname(),
                            typing,
                        );
                    } else if self
                        .message_query_target(&message.command)
                        .as_ref()
                        .is_some_and(|query| {
                            query.as_normalized_str()
                                == self.nickname().as_normalized_str()
                        })
                    {
                        self.handle_query_typing_tagmsg(
                            &target::Query::from(&sender),
                            typing,
                        );
                    }
                }

                return Ok(vec![]);
            }
            Command::ACCOUNT(accountname) => {
                let old_user = ok!(message.user(self.casemapping()));

                self.chanmap.values_mut().for_each(|channel| {
                    if let Some(user) = channel.users.take(&old_user) {
                        channel
                            .users
                            .insert(user.with_accountname(accountname));
                    }
                });

                if old_user.nickname() == self.nickname()
                    && accountname != "*"
                    && !self.registration_required_channels.is_empty()
                {
                    for message in group_joins(
                        &self.registration_required_channels,
                        &self.config.channel_keys,
                        find_target_limit(&self.isupport, "JOIN"),
                    ) {
                        self.handle.try_send(message)?;
                    }

                    self.registration_required_channels.clear();
                }
            }
            Command::CHGHOST(new_username, new_hostname) => {
                let old_user = ok!(message.user(self.casemapping()));

                let ourself = old_user.nickname() == self.nickname();

                if ourself {
                    self.resolved_user = Some(new_username.to_string());
                    self.resolved_host = Some(new_hostname.to_string());
                }

                self.chanmap.values_mut().for_each(|channel| {
                    if let Some(user) = channel.users.take(&old_user) {
                        channel.users.insert(user.with_username_and_hostname(
                            new_username.clone(),
                            new_hostname.clone(),
                        ));
                    }
                });

                let channels = self.user_channels(old_user.nickname());

                let (server_time, received_with_server_time) =
                    message.server_time_or_now();

                return Ok(vec![Event::Broadcast(Broadcast::ChangeHost {
                    old_user,
                    new_username: new_username.clone(),
                    new_hostname: new_hostname.clone(),
                    ourself,
                    logged_in: self.logged_in,
                    channels,
                    server_time,
                    received_with_server_time,
                })]);
            }
            Command::Numeric(RPL_VISIBLEHOST, args) => {
                let hostname = ok!(args.get(1));

                if let Some((username, hostname)) = hostname.split_once('@') {
                    self.resolved_user = Some(username.to_string());
                    self.resolved_host = Some(hostname.to_string());
                } else {
                    self.resolved_host = Some(hostname.to_string());
                }
            }
            Command::SETNAME(_realname) => {
                // Real names are not currently stored anywhere, but
                // SETNAME provides an opportunity to update resolved_user
                // and resolved_host.
                let user = ok!(message.user(self.casemapping()));

                let ourself = user.nickname() == self.nickname();

                if ourself {
                    if let Some(username) = user.username() {
                        self.resolved_user = Some(username.to_string());
                    }

                    if let Some(hostname) = user.hostname() {
                        self.resolved_host = Some(hostname.to_string());
                    }
                }
            }
            Command::Numeric(RPL_MONONLINE, args) => {
                let casemapping =
                    isupport::get_casemapping_or_default(&self.isupport);
                let prefix = isupport::get_prefix(&self.isupport);

                let targets = ok!(args.get(1))
                    .split(',')
                    .filter_map(|target| {
                        let user =
                            User::parse_or_force(target, casemapping, prefix);
                        let entry = self
                            .monitored_users
                            .entry(user.clone())
                            .and_modify(|monitored_user| {
                                monitored_user.online = true;
                            })
                            .or_insert_with(|| MonitoredUser {
                                online: true,
                                automated: false,
                            });
                        (!entry.automated).then_some(user)
                    })
                    .collect::<Vec<_>>();

                let events = if !targets.is_empty() {
                    vec![
                        Event::Single {
                            message: message.clone(),
                            our_nick: self.nickname().to_owned(),
                            deduplicate: false,
                        },
                        Event::MonitoredOnline(targets),
                    ]
                } else {
                    vec![]
                };
                return Ok(events);
            }
            Command::Numeric(RPL_MONOFFLINE, args) => {
                let casemapping =
                    isupport::get_casemapping_or_default(&self.isupport);
                let prefix = isupport::get_prefix(&self.isupport);
                let targets = ok!(args.get(1))
                    .split(',')
                    .filter_map(|target| {
                        let nick = Nick::from_str(target, casemapping);
                        let entry = self
                            .monitored_users
                            .entry(User::parse_or_force(
                                target,
                                casemapping,
                                prefix,
                            ))
                            .and_modify(|monitored_user| {
                                monitored_user.online = false;
                            })
                            .or_insert_with(|| MonitoredUser {
                                online: false,
                                automated: false,
                            });
                        (!entry.automated).then_some(nick)
                    })
                    .collect::<Vec<_>>();

                let events = if !targets.is_empty() {
                    vec![
                        Event::Single {
                            message: message.clone(),
                            our_nick: self.nickname().to_owned(),
                            deduplicate: false,
                        },
                        Event::MonitoredOffline(targets),
                    ]
                } else {
                    vec![]
                };
                return Ok(events);
            }
            Command::Numeric(RPL_ENDOFMONLIST, _) => {
                return Ok(vec![]);
            }
            Command::MARKREAD(target, Some(timestamp)) => {
                if let Some(read_marker) = timestamp
                    .strip_prefix("timestamp=")
                    .and_then(|timestamp| timestamp.parse::<ReadMarker>().ok())
                {
                    return Ok(vec![Event::UpdateReadMarker(
                        Target::parse(
                            target,
                            self.chantypes(),
                            self.statusmsg(),
                            self.casemapping(),
                        ),
                        read_marker,
                    )]);
                }
            }
            Command::CHATHISTORY(sub, args) => {
                let mut events = vec![];

                if sub == "TARGETS" {
                    let target = Target::parse(
                        ok!(args.first()),
                        self.chantypes(),
                        self.statusmsg(),
                        self.casemapping(),
                    );

                    let timestamp = ok!(args.get(1));

                    if let Ok(server_time) =
                        DateTime::parse_from_rfc3339(timestamp)
                            .map(|date_time| date_time.to_utc())
                    {
                        events.push(Event::ChatHistoryTargetReceived(
                            target,
                            server_time,
                        ));
                    }

                    if self.chathistory_targets_request.is_none() {
                        // User requested, save to history
                        events.push(Event::Single {
                            message: message.clone(),
                            our_nick: self.nickname().to_owned(),
                            deduplicate: false,
                        });
                    }
                }

                return Ok(events);
            }
            Command::Numeric(RPL_SASLSUCCESS, _) => {
                self.sasl_succeeded = true;
                self.registration_step = RegistrationStep::End;
                self.handle.try_send(command!("CAP", "END"))?;
            }
            Command::Numeric(ERR_SASLFAIL | ERR_SASLTOOLONG, _) => {
                log::warn!("[{}] SASL authentication failed", self.server);

                if self
                    .config
                    .sasl
                    .as_ref()
                    .is_some_and(config::server::Sasl::disconnect_on_failure)
                {
                    log::warn!(
                        "[{}] disconnected in order to protect identity from SASL authentication failure",
                        self.server
                    );

                    return Ok(vec![Event::Disconnect(Some(
                        "SASL authentication failure".to_string(),
                    ))]);
                }

                self.registration_step = RegistrationStep::End;
                self.handle.try_send(command!("CAP", "END"))?;
            }
            Command::Numeric(RPL_TRYAGAIN, args) => {
                let command = ok!(args.get(1));

                if command == "WHO" && self.config.who_poll_enabled {
                    self.who_poll_interval.too_short();

                    log::debug!(
                        "[{}] WHO poll interval is too short → duration = {:?}",
                        self.server,
                        self.who_poll_interval.duration()
                    );

                    if !self.who_polls.iter().any(|who_poll| {
                        matches!(
                            who_poll.status,
                            WhoStatus::Requested(WhoSource::User, _, _)
                        )
                    }) {
                        // No user request, rate-limited due to WHO polling
                        return Ok(vec![]);
                    }
                }
            }
            Command::Numeric(RPL_ENDOFMOTD | ERR_NOMOTD, _)
                if self.registration_step != RegistrationStep::Complete =>
            {
                // MOTD (or ERR_NOMOTD) is the last required message in the numerics
                // sent on successfully completing the registration process (after
                // RPL_ISUPPORT message(s) are sent).
                // https://modern.ircdocs.horse/#connection-registration
                if self.registration_step != RegistrationStep::End {
                    log::warn!(
                        "[{}] Registration completed while in mode: {:?}",
                        self.server,
                        self.registration_step
                    );
                }

                self.registration_step = RegistrationStep::Complete;

                if let Some(id) = self.server.bouncer_netid()
                    && self.resolved_netid.is_none()
                {
                    // we want to be a bouncer network, but we never connected to one.
                    bail!("Requested bouncer id {id}, but was not connected.");
                }

                // Send nick password & ghost
                if let Some(nick_pass) = self.config.nick_password.as_ref() {
                    // Try ghost recovery if we couldn't claim our nick
                    if self.config.should_ghost
                        && self.resolved_nick.as_ref().is_some_and(
                            |resolved_nick| {
                                *resolved_nick == self.configured_nick
                            },
                        )
                    {
                        for sequence in &self.config.ghost_sequence {
                            self.handle.try_send(command!(
                                "PRIVMSG",
                                "NickServ",
                                format!(
                                    "{sequence} {} {nick_pass}",
                                    &self.config.nickname
                                )
                            ))?;
                        }
                    }

                    if let Some(identify_syntax) =
                        &self.config.nick_identify_syntax
                    {
                        match identify_syntax {
                            config::server::IdentifySyntax::PasswordNick => {
                                self.handle.try_send(command!(
                                    "PRIVMSG",
                                    "NickServ",
                                    format!(
                                        "IDENTIFY {nick_pass} {}",
                                        &self.config.nickname
                                    )
                                ))?;
                            }
                            config::server::IdentifySyntax::NickPassword => {
                                self.handle.try_send(command!(
                                    "PRIVMSG",
                                    "NickServ",
                                    format!(
                                        "IDENTIFY {} {nick_pass}",
                                        &self.config.nickname
                                    )
                                ))?;
                            }
                        }
                    } else if self.resolved_nick.as_ref().is_some_and(
                        |resolved_nick| *resolved_nick == self.configured_nick,
                    ) {
                        // Use nickname-less identification if possible, since it has
                        // no possible argument order issues.
                        self.handle.try_send(command!(
                            "PRIVMSG",
                            "NickServ",
                            format!("IDENTIFY {nick_pass}")
                        ))?;
                    } else {
                        // Default to most common syntax if unknown
                        self.handle.try_send(command!(
                            "PRIVMSG",
                            "NickServ",
                            format!(
                                "IDENTIFY {} {nick_pass}",
                                &self.config.nickname
                            )
                        ))?;
                    }
                }

                // Send user modestring
                if let (Some(nick), Some(modestring)) =
                    (self.resolved_nick.clone(), self.config.umodes.as_ref())
                {
                    self.handle.try_send(command!(
                        "MODE",
                        nick.to_string(),
                        modestring
                    ))?;
                }

                // Request bouncer networks
                // TODO(pounce) replace this with "bouncer-networks-notify" after the cap handling
                // is cleaned up.
                if self.is_primary()
                    && self
                        .capabilities
                        .acknowledged(Capability::BouncerNetworks)
                {
                    self.handle
                        .try_send(command!("BOUNCER", "LISTNETWORKS"))?;
                }

                // request metadata
                if self.capabilities.acknowledged(Capability::Metadata) {
                    let mut requested = config
                        .metadata
                        .preferred_key_strs()
                        .map(ToString::to_string)
                        .collect::<Vec<String>>();

                    if let Some(limits) = self.capabilities.metadata_limits() {
                        requested.truncate(limits.max_subs);
                    }

                    log::debug!(
                        "[{}] Requesting subs: {requested:?}",
                        self.server
                    );

                    if !requested.is_empty() {
                        for key in &requested {
                            self.metadata_sub_requests.insert(key.to_string());
                        }

                        let mut args = vec!["*".to_string(), "SUB".to_string()];
                        args.extend(requested);

                        self.handle.try_send(command("METADATA", args))?;
                    }
                }

                let channels = self
                    .config
                    .channels
                    .iter()
                    .filter_map(|channel| {
                        target::Channel::parse(
                            channel,
                            self.chantypes(),
                            self.statusmsg(),
                            self.casemapping(),
                        )
                        .ok()
                    })
                    .collect::<Vec<_>>();

                // Send JOIN on non bouncer networks
                if !self.capabilities.acknowledged(Capability::BouncerNetworks)
                {
                    for message in group_joins(
                        &channels,
                        &self.config.channel_keys,
                        find_target_limit(&self.isupport, "JOIN"),
                    ) {
                        self.handle.try_send(message)?;
                    }
                }

                if !self.config.monitor.is_empty() {
                    if let Some(isupport::Parameter::MONITOR(monitor_limit)) =
                        self.isupport.get(&isupport::Kind::MONITOR)
                    {
                        let messages = group_monitors(
                            &self.config.monitor,
                            *monitor_limit,
                            find_target_limit(&self.isupport, "MONITOR"),
                            &self.server,
                            0,
                        );
                        for message in messages {
                            self.handle.try_send(message)?;
                        }
                    } else {
                        log::warn!(
                            "[{}] Monitor list configured for, but is not supported by the server",
                            self.server,
                        );
                    }
                }

                let events = self
                    .config
                    .queries
                    .iter()
                    .filter_map(|query| {
                        target::Query::parse(
                            query,
                            self.chantypes(),
                            self.statusmsg(),
                            self.casemapping(),
                        )
                        .ok()
                        .map(Event::AddToSidebar)
                    })
                    .chain(iter::once(Event::OnConnect(on_connect(
                        self.handle.clone(),
                        self.config.clone(),
                        self.nickname(),
                        &self.isupport,
                        &self.capabilities,
                        &self.features,
                        self.filehost(),
                        config,
                    ))))
                    .collect::<Vec<_>>();

                if (!self
                    .capabilities
                    .acknowledged(Capability::BouncerNetworks)
                    || !self.is_primary())
                    && matches!(
                        self.features.version_request,
                        VersionRequest::Need(true)
                    )
                {
                    self.features.version_request = VersionRequest::Sent;

                    self.send(
                        None,
                        command!("VERSION").into(),
                        TokenPriority::Low,
                    );
                }

                if !self.config.metadata.is_empty() {
                    if self.capabilities.acknowledged(Capability::Metadata) {
                        for (key, value) in
                            self.config.metadata.clone().into_iter()
                        {
                            self.send(
                                None,
                                command!(
                                    "METADATA",
                                    self.nickname().to_string(),
                                    "SET",
                                    key.to_str(),
                                    value
                                )
                                .into(),
                                TokenPriority::High,
                            );
                        }
                    } else {
                        log::warn!(
                            "[{}] Metadata configured for, but is not supported by the server",
                            self.server,
                        );
                    }
                }

                return Ok(events);
            }
            Command::Numeric(RPL_VERSION, args) => {
                if matches!(self.features.version_request, VersionRequest::Sent)
                {
                    self.features.version_request = VersionRequest::Need(false);

                    let server_version = ok!(args.get(1));

                    self.features.enable_supported(server_version);

                    return Ok(vec![]);
                }
            }
            Command::METADATA(target, args) => {
                if let [key, _visibility, value] = &args[..] {
                    log::debug!(
                        "[{}] Received metadata [{target}]: {key}={value}",
                        self.server
                    );
                    self.registry.insert(
                        Target::parse(
                            target,
                            self.chantypes(),
                            self.statusmsg(),
                            self.casemapping(),
                        ),
                        key.clone(),
                        value.clone(),
                    );
                }
            }
            Command::Numeric(RPL_KEYVALUE, args) => {
                if let [_, target, key, _visibility, value] = &args[..] {
                    log::debug!(
                        "[{}] Received metadata [{target}]: {key}={value}",
                        self.server
                    );
                    self.registry.insert(
                        Target::parse(
                            target,
                            self.chantypes(),
                            self.statusmsg(),
                            self.casemapping(),
                        ),
                        key.clone(),
                        value.clone(),
                    );
                    return Ok(vec![]);
                }
            }
            Command::FAIL(command, code, context, _) => {
                if command == "METADATA"
                    && code == "KEY_INVALID"
                    && let Some(key) =
                        context.as_ref().and_then(|context| context.first())
                    && self.metadata_sub_requests.take(key).is_some()
                {
                    log::info!(
                        "[{}] Metadata {key} not supported by the server",
                        self.server
                    );

                    // Expected as part of requesting keys via METADATA SUB,
                    // hide from the user.
                    return Ok(vec![]);
                }
            }
            Command::Numeric(RPL_METADATASUBOK, args) => {
                for key in args.iter().skip(1) {
                    log::info!(
                        "[{}] Metadata {key} subscription successful",
                        self.server
                    );

                    self.metadata_sub_requests.remove(key);
                }

                return Ok(vec![]);
            }
            Command::Numeric(RPL_METADATASYNCLATER, args) => {
                // args: <Client> <Target> [<RetryAfter>]
                // https://ircv3.net/specs/extensions/metadata#postponed-synchronization
                if let Some(target) = args.get(1) {
                    let target = Target::parse(
                        target,
                        self.chantypes(),
                        self.statusmsg(),
                        self.casemapping(),
                    );

                    // waiting at least <RetryAfter> seconds if provided
                    let retry_after = args
                        .get(2)
                        .and_then(|secs| secs.parse::<u64>().ok())
                        .map_or(Duration::ZERO, Duration::from_secs);

                    let ready_at = Instant::now() + retry_after;

                    log::debug!(
                        "[{}] Metadata sync postponed for [{target}], retrying in {retry_after:?}",
                        self.server
                    );

                    if !self
                        .metadata_syncs
                        .iter()
                        .any(|sync| sync.target == target)
                    {
                        self.metadata_syncs
                            .push(MetadataSync { target, ready_at });
                    }
                }

                return Ok(vec![]);
            }
            _ => {}
        }

        if let Some(target) =
            context.map(Context::buffer).as_ref().map(Destination::from)
        {
            Ok(vec![Event::WithTarget {
                message,
                our_nick: self.nickname().to_owned(),
                target,
                deduplicate: false,
            }])
        } else {
            Ok(vec![Event::Single {
                message,
                our_nick: self.nickname().to_owned(),
                deduplicate: false,
            }])
        }
    }

    fn handle_chathistory(
        &mut self,
        message: message::Encoded,
        batch_target: Target,
    ) -> Vec<Event> {
        if Some(User::from(Nick::from_str("HistServ", self.casemapping())))
            == message.user(self.casemapping())
        {
            // HistServ provides event-playback without event-playback which
            // would require client-side parsing to map appropriately. Avoid
            // that complexity by only providing that functionality via
            // event-playback.
            vec![]
        } else {
            match &message.command {
                _ if is_reaction(&message) => {
                    vec![Event::Reaction {
                        message,
                        our_nick: self.nickname().to_owned(),
                        notification_enabled: false,
                        deduplicate: true,
                        labeled_response_context: None,
                    }]
                }
                Command::REDACT(_, _, _) => {
                    vec![Event::Redaction(message, self.nickname().to_owned())]
                }
                Command::NICK(_) => vec![Event::WithTarget {
                    message,
                    our_nick: self.nickname().to_owned(),
                    target: batch_target.into(),
                    deduplicate: true,
                }],
                Command::JOIN(_, _) => vec![Event::WithTarget {
                    message,
                    our_nick: self.nickname().to_owned(),
                    target: batch_target.into(),
                    deduplicate: true,
                }],
                Command::PART(_, _) => vec![Event::WithTarget {
                    message,
                    our_nick: self.nickname().to_owned(),
                    target: batch_target.into(),
                    deduplicate: true,
                }],
                Command::QUIT(_) => vec![Event::WithTarget {
                    message,
                    our_nick: self.nickname().to_owned(),
                    target: batch_target.into(),
                    deduplicate: true,
                }],
                Command::PRIVMSG(target, text)
                | Command::NOTICE(target, text) => {
                    if ctcp::is_query(text) && !message::is_action(text) {
                        // Ignore historical CTCP queries/responses except for
                        // ACTIONs
                        vec![]
                    } else {
                        if let Some(user) = message.user(self.casemapping()) {
                            // If direct message, update query map with user
                            if target == &self.nickname().to_string() {
                                self.record_query(&target::Query::from(user));
                            }
                        }

                        vec![Event::PrivOrNotice {
                            message,
                            our_nick: self.nickname().to_owned(),
                            // Don't allow notifications from history
                            notification_enabled: false,
                            deduplicate: true,
                            labeled_response_context: None,
                        }]
                    }
                }
                _ => vec![Event::Single {
                    message,
                    our_nick: self.nickname().to_owned(),
                    deduplicate: true,
                }],
            }
        }
    }

    fn handle_multiline(&mut self, message: message::Encoded, batch_tag: &str) {
        if let Some(batch) = self.batches.get_mut(batch_tag)
            && let Some(BatchKind::Multiline(
                _,
                _,
                _,
                ref mut kind,
                ref mut text,
            )) = batch.kind
        {
            match &message.command {
                Command::PRIVMSG(_, message_text) => {
                    if let Some(kind) = kind {
                        if !matches!(kind, MultilineBatchKind::PRIVMSG) {
                            log::warn!(
                                "[{}] Ignoring invalid draft/multiline batch message {message:?}",
                                self.server
                            );

                            return;
                        }

                        if !message.tags.contains_key("draft/multiline-concat")
                        {
                            text.push('\n');
                        }
                    } else {
                        *kind = Some(MultilineBatchKind::PRIVMSG);
                    }

                    text.push_str(message_text.as_str());
                }
                Command::NOTICE(_, message_text) => {
                    if let Some(kind) = kind {
                        if !matches!(kind, MultilineBatchKind::NOTICE) {
                            log::warn!(
                                "[{}] Ignoring invalid draft/multiline batch message {message:?}",
                                self.server
                            );

                            return;
                        }

                        if !message.tags.contains_key("draft/multiline-concat")
                        {
                            text.push('\n');
                        }
                    } else {
                        *kind = Some(MultilineBatchKind::NOTICE);
                    }

                    text.push_str(message_text.as_str());
                }
                _ => log::warn!(
                    "[{}] Ignoring invalid draft/multiline batch message {message:?}",
                    self.server
                ),
            }
        }
    }

    fn handle_znc_playback(
        &mut self,
        message: message::Encoded,
        batch_target: Target,
    ) -> Vec<Event> {
        match &message.command {
            _ if is_reaction(&message) => {
                vec![Event::Reaction {
                    message,
                    our_nick: self.nickname().to_owned(),
                    notification_enabled: false,
                    deduplicate: true,
                    labeled_response_context: None,
                }]
            }
            Command::REDACT(_, _, _) => {
                vec![Event::Redaction(message, self.nickname().to_owned())]
            }
            Command::NICK(_) => vec![Event::WithTarget {
                message,
                our_nick: self.nickname().to_owned(),
                target: batch_target.into(),
                deduplicate: true,
            }],
            Command::JOIN(_, _) => vec![Event::WithTarget {
                message,
                our_nick: self.nickname().to_owned(),
                target: batch_target.into(),
                deduplicate: true,
            }],
            Command::PART(_, _) => vec![Event::WithTarget {
                message,
                our_nick: self.nickname().to_owned(),
                target: batch_target.into(),
                deduplicate: true,
            }],
            Command::QUIT(_) => vec![Event::WithTarget {
                message,
                our_nick: self.nickname().to_owned(),
                target: batch_target.into(),
                deduplicate: true,
            }],
            Command::PRIVMSG(target, text) | Command::NOTICE(target, text) => {
                if ctcp::is_query(text) && !message::is_action(text) {
                    // Ignore historical CTCP queries/responses except for
                    // ACTIONs
                    vec![]
                } else {
                    if let Some(user) = message.user(self.casemapping()) {
                        // If direct message, update query map with user
                        if target == &self.nickname().to_string() {
                            self.record_query(&target::Query::from(user));
                        }
                    }

                    vec![Event::PrivOrNotice {
                        message,
                        our_nick: self.nickname().to_owned(),
                        // Don't allow notifications from history
                        notification_enabled: false,
                        deduplicate: true,
                        labeled_response_context: None,
                    }]
                }
            }
            _ => vec![Event::Single {
                message,
                our_nick: self.nickname().to_owned(),
                deduplicate: true,
            }],
        }
    }

    fn send_markread(
        &mut self,
        target: Target,
        read_marker: ReadMarker,
        priority: TokenPriority,
    ) {
        if self.capabilities.acknowledged(Capability::ReadMarker) {
            self.send(
                None,
                command!(
                    "MARKREAD",
                    target.as_str().to_string(),
                    format!("timestamp={read_marker}"),
                )
                .into(),
                priority,
            );
        }
    }

    fn user_who_request(&self, channel: &target::Channel) -> bool {
        if let Some(who_poll) = self
            .who_polls
            .iter()
            .find(|who_poll| who_poll.channel == *channel)
        {
            matches!(
                who_poll.status,
                WhoStatus::Requested(WhoSource::User, _, _)
                    | WhoStatus::Receiving(WhoSource::User, _)
            )
        } else {
            false
        }
    }

    pub fn chathistory_limit(&self) -> u16 {
        if let Some(isupport::Parameter::CHATHISTORY(server_limit)) =
            self.isupport.get(&isupport::Kind::CHATHISTORY)
            && *server_limit != 0
        {
            return std::cmp::min(*server_limit, CLIENT_CHATHISTORY_LIMIT);
        }

        CLIENT_CHATHISTORY_LIMIT
    }

    pub fn chathistory_message_reference_types(
        &self,
    ) -> Vec<isupport::MessageReferenceType> {
        if let Some(isupport::Parameter::MSGREFTYPES(message_reference_types)) =
            self.isupport.get(&isupport::Kind::MSGREFTYPES)
        {
            message_reference_types.clone()
        } else {
            vec![]
        }
    }

    pub fn chathistory_request(
        &self,
        target: &Target,
    ) -> Option<ChatHistorySubcommand> {
        self.chathistory_requests
            .get(target)
            .map(|request| request.subcommand.clone())
    }

    pub fn send_chathistory_request(
        &mut self,
        subcommand: ChatHistorySubcommand,
        priority: TokenPriority,
    ) {
        if self.capabilities.acknowledged(Capability::Chathistory) {
            if let Some(target) = subcommand.target() {
                if self.pending_chathistory_requests.contains_key(target) {
                    return;
                } else if !self.chathistory_requests.contains_key(target) {
                    let autorequest = !matches!(priority, TokenPriority::User);

                    // If the chathistory request is an automatic request due to
                    // joining a channel, then we want to wait for the initial,
                    // implicit NAMES reply to complete before requesting
                    // chathistory so that we have as much channel state as
                    // possible when parsing messages.
                    if autorequest
                        && let Some(channel) = target
                            .as_channel()
                            .and_then(|channel| self.chanmap.get_mut(channel))
                        && !channel.names_init
                    {
                        self.pending_chathistory_requests
                            .insert(target.clone(), (subcommand, priority));

                        return;
                    }

                    self.chathistory_requests.insert(
                        target.clone(),
                        ChatHistoryRequest {
                            subcommand: subcommand.clone(),
                            requested_at: Instant::now(),
                            autorequest: !matches!(
                                priority,
                                TokenPriority::User
                            ),
                        },
                    );
                } else {
                    return;
                }
            } else if self.chathistory_targets_request.is_some() {
                return;
            } else {
                self.chathistory_targets_request = Some(ChatHistoryRequest {
                    subcommand: subcommand.clone(),
                    requested_at: Instant::now(),
                    autorequest: !matches!(priority, TokenPriority::User),
                });
            }

            match subcommand {
                ChatHistorySubcommand::Latest(
                    target,
                    message_reference,
                    limit,
                ) => {
                    let command_message_reference =
                        message_reference.map(|message_reference| {
                            isupport::fuzz_start_message_reference(
                                message_reference,
                            )
                        });

                    log::debug!(
                        "[{}] requesting {limit} latest messages in {target} since {}",
                        self.server,
                        format_optional_message_reference(
                            &command_message_reference
                        ),
                    );

                    self.send(
                        None,
                        command!(
                            "CHATHISTORY",
                            "LATEST",
                            target.to_string(),
                            format_optional_message_reference(
                                &command_message_reference
                            ),
                            limit.to_string(),
                        )
                        .into(),
                        priority,
                    );
                }
                ChatHistorySubcommand::Before(
                    target,
                    message_reference,
                    limit,
                ) => {
                    let command_message_reference =
                        isupport::fuzz_end_message_reference(message_reference);

                    log::debug!(
                        "[{}] requesting {limit} messages in {target} before {}",
                        self.server,
                        command_message_reference,
                    );

                    self.send(
                        None,
                        command!(
                            "CHATHISTORY",
                            "BEFORE",
                            target.to_string(),
                            command_message_reference.to_string(),
                            limit.to_string(),
                        )
                        .into(),
                        priority,
                    );
                }
                ChatHistorySubcommand::Between(
                    target,
                    start_message_reference,
                    end_message_reference,
                    limit,
                ) => {
                    let (
                        command_start_message_reference,
                        command_end_message_reference,
                    ) = isupport::fuzz_message_reference_range(
                        start_message_reference,
                        end_message_reference,
                    );

                    log::debug!(
                        "[{}] requesting {limit} messages in {target} between {} and {}",
                        self.server,
                        command_start_message_reference,
                        command_end_message_reference,
                    );

                    self.send(
                        None,
                        command!(
                            "CHATHISTORY",
                            "BETWEEN",
                            target.to_string(),
                            command_start_message_reference.to_string(),
                            command_end_message_reference.to_string(),
                            limit.to_string(),
                        )
                        .into(),
                        priority,
                    );
                }
                ChatHistorySubcommand::Targets(
                    start_timestamp,
                    end_timestamp,
                    limit,
                ) => {
                    let command_start_message_reference =
                        isupport::MessageReference::Timestamp(start_timestamp);

                    let command_end_message_reference =
                        isupport::MessageReference::Timestamp(end_timestamp);

                    let (
                        command_start_message_reference,
                        command_end_message_reference,
                    ) = isupport::fuzz_message_reference_range(
                        command_start_message_reference,
                        command_end_message_reference,
                    );

                    log::debug!(
                        "[{}] requesting {limit} targets between {} and {}",
                        self.server,
                        command_start_message_reference,
                        command_end_message_reference,
                    );

                    self.send(
                        None,
                        command!(
                            "CHATHISTORY",
                            "TARGETS",
                            command_start_message_reference.to_string(),
                            command_end_message_reference.to_string(),
                            limit.to_string(),
                        )
                        .into(),
                        priority,
                    );
                }
            }
        }
    }

    pub fn clear_chathistory_request(&mut self, target: Option<&Target>) {
        if let Some(target) = target {
            self.chathistory_requests.remove(target);
        } else {
            self.chathistory_targets_request = None;
        }
    }

    fn finish_chathistory_request(
        &mut self,
        target: Option<&Target>,
        size: u16,
        events: &[Event],
        chathistory_end: bool,
    ) -> Option<ChatHistorySubcommand> {
        if let Some(ChatHistoryRequest {
            subcommand,
            autorequest,
            ..
        }) = if let Some(target) = target {
            self.chathistory_requests.get(target)
        } else {
            self.chathistory_targets_request.as_ref()
        } {
            if let Some(target) = target
                && let ChatHistorySubcommand::Before(_, _, limit)
                | ChatHistorySubcommand::Latest(_, None, limit) = subcommand
            {
                self.chathistory_exhausted
                    .insert(target.clone(), chathistory_end || size < *limit);
            }

            match subcommand {
                ChatHistorySubcommand::Latest(
                    target,
                    message_reference,
                    limit,
                ) => {
                    log::debug!(
                        "[{}] received latest {} messages in {} since {}",
                        self.server,
                        size,
                        target,
                        format_optional_message_reference(message_reference),
                    );

                    if let Some(message_reference) = message_reference
                        && *autorequest
                        && size >= *limit
                        && !chathistory_end
                    {
                        continue_chathistory_between(
                            target,
                            events,
                            None,
                            message_reference,
                            self.chathistory_limit(),
                        )
                    } else {
                        None
                    }
                }
                ChatHistorySubcommand::Before(target, message_reference, _) => {
                    log::debug!(
                        "[{}] received {} messages in {} before {}",
                        self.server,
                        size,
                        target,
                        message_reference,
                    );

                    None
                }
                ChatHistorySubcommand::Between(
                    target,
                    start_message_reference,
                    end_message_reference,
                    limit,
                ) => {
                    log::debug!(
                        "[{}] received {} messages in {} between {} and {}",
                        self.server,
                        size,
                        target,
                        start_message_reference,
                        end_message_reference,
                    );

                    if *autorequest && size >= *limit && !chathistory_end {
                        continue_chathistory_between(
                            target,
                            events,
                            Some(start_message_reference),
                            end_message_reference,
                            self.chathistory_limit(),
                        )
                    } else {
                        None
                    }
                }
                ChatHistorySubcommand::Targets(
                    start_timestamp,
                    end_timestamp,
                    limit,
                ) => {
                    log::debug!(
                        "[{}] received {} targets between {} and {}",
                        self.server,
                        size,
                        MessageReference::Timestamp(*start_timestamp),
                        MessageReference::Timestamp(*end_timestamp),
                    );

                    if *autorequest && size >= *limit && !chathistory_end {
                        continue_chathistory_targets(
                            events,
                            start_timestamp,
                            end_timestamp,
                            self.chathistory_limit(),
                        )
                    } else {
                        None
                    }
                }
            }
        } else {
            None
        }
    }

    pub fn chathistory_exhausted(&self, target: &Target) -> bool {
        self.chathistory_exhausted
            .get(target)
            .copied()
            .unwrap_or_default()
    }

    pub fn load_chathistory_targets_timestamp(
        &self,
        server_time: DateTime<Utc>,
    ) -> impl Future<Output = Message> + use<> {
        let server = self.server.clone();

        let limit = self.chathistory_limit();

        async move {
            let timestamp = load_chathistory_targets_timestamp(server.clone())
                .await
                .ok()
                .flatten();

            let start_timestamp = timestamp.unwrap_or(DateTime::UNIX_EPOCH);

            let end_timestamp = server_time;

            Message::ChatHistoryRequest(
                server,
                ChatHistorySubcommand::Targets(
                    start_timestamp,
                    end_timestamp,
                    limit,
                ),
            )
        }
        .boxed()
    }

    pub fn overwrite_chathistory_targets_timestamp(
        &self,
        timestamp: DateTime<Utc>,
    ) -> impl Future<Output = Message> + use<> {
        let server = self.server.clone();

        async move {
            let result = overwrite_chathistory_targets_timestamp(
                server.clone(),
                timestamp,
            )
            .await;

            Message::ChatHistoryTargetsTimestampUpdated(
                server, timestamp, result,
            )
        }
        .boxed()
    }

    pub fn channels(&self) -> impl Iterator<Item = &target::Channel> {
        self.chanmap.keys()
    }

    fn topic<'a>(&'a self, channel: &target::Channel) -> Option<&'a Topic> {
        self.chanmap.get(channel).map(|channel| &channel.topic)
    }

    fn mode<'a>(&'a self, channel: &target::Channel) -> Option<&'a String> {
        self.chanmap
            .get(channel)
            .and_then(|channel| channel.mode.as_ref())
    }

    fn resolve_user_attributes<'a>(
        &'a self,
        channel: &target::Channel,
        user: &User,
    ) -> Option<&'a User> {
        self.chanmap
            .get(channel)
            .and_then(|channel| channel.users.resolve(user))
    }

    pub fn users<'a>(
        &'a self,
        channel: &target::Channel,
    ) -> Option<&'a ChannelUsers> {
        self.chanmap.get(channel).map(|chanimpl| &chanimpl.users)
    }

    pub fn channel_typing_users(
        &self,
        channel: &target::Channel,
    ) -> Vec<String> {
        let mut users = self
            .chanmap
            .get(channel)
            .map(|channel| {
                channel
                    .typing
                    .iter()
                    .filter(|(_, updated_at)| !is_typing_expired(**updated_at))
                    .map(|(nick, _)| nick.to_string())
                    .collect_vec()
            })
            .unwrap_or_default();

        users.sort_unstable();
        users
    }

    pub fn has_channel_typing_users(&self, channel: &target::Channel) -> bool {
        self.chanmap.get(channel).is_some_and(|channel| {
            channel
                .typing
                .values()
                .any(|updated_at| !is_typing_expired(*updated_at))
        })
    }

    pub fn query_typing_users(&self, query: &target::Query) -> Vec<String> {
        self.querymap
            .get(query)
            .and_then(|query_state| query_state.typing)
            .filter(|updated_at| !is_typing_expired(*updated_at))
            .map(|_| vec![query.as_str().to_string()])
            .unwrap_or_default()
    }

    pub fn has_query_typing_users(&self, query: &target::Query) -> bool {
        self.querymap
            .get(query)
            .and_then(|query_state| query_state.typing)
            .is_some_and(|updated_at| !is_typing_expired(updated_at))
    }

    fn user_channels(&self, nick: NickRef) -> Vec<target::Channel> {
        self.chanmap
            .iter()
            .filter(|(_, chan)| chan.users.get_by_nick(nick).is_some())
            .map(|(t, _)| t)
            .cloned()
            .collect()
    }

    fn resolve_query<'a>(
        &'a self,
        query: &target::Query,
    ) -> Option<&'a target::Query> {
        self.querymap
            .get_key_value(query)
            .map(|(stored_query, query_state)| {
                query_state.query.as_ref().unwrap_or(stored_query)
            })
    }

    fn record_query(&mut self, query: &target::Query) {
        prune_expired_querymap(&mut self.querymap);
        self.querymap.entry(query.clone()).or_default().query =
            Some(query.clone());
    }

    fn update_channel_typing(
        &mut self,
        channel: &target::Channel,
        nick: NickRef<'_>,
    ) {
        if let Some(channel) = self.chanmap.get_mut(channel) {
            prune_expired_typing(&mut channel.typing);
            channel.typing.insert(nick.to_owned(), Instant::now());
        }
    }

    fn clear_channel_typing(
        &mut self,
        channel: &target::Channel,
        nick: NickRef<'_>,
    ) {
        if let Some(channel) = self.chanmap.get_mut(channel) {
            prune_expired_typing(&mut channel.typing);
            channel.typing.remove(&nick.to_owned());
        }
    }

    fn update_query_typing(&mut self, query: &target::Query) {
        prune_expired_querymap(&mut self.querymap);
        self.querymap.entry(query.clone()).or_default().typing =
            Some(Instant::now());
    }

    fn clear_query_typing(&mut self, query: &target::Query) {
        prune_expired_querymap(&mut self.querymap);

        if let Some(query_state) = self.querymap.get_mut(query) {
            query_state.typing = None;
        }

        self.querymap
            .retain(|_, query_state| !query_state.is_empty());
    }

    fn message_channel_target(
        &self,
        command: &Command,
    ) -> Option<target::Channel> {
        let target = match command {
            Command::TAGMSG(target)
            | Command::PRIVMSG(target, _)
            | Command::NOTICE(target, _) => target,
            _ => return None,
        };

        target::Channel::parse(
            target,
            self.chantypes(),
            self.statusmsg(),
            self.casemapping(),
        )
        .ok()
    }

    fn message_query_target(&self, command: &Command) -> Option<target::Query> {
        let target = match command {
            Command::TAGMSG(target)
            | Command::PRIVMSG(target, _)
            | Command::NOTICE(target, _) => target,
            _ => return None,
        };

        target::Query::parse(
            target,
            self.chantypes(),
            self.statusmsg(),
            self.casemapping(),
        )
        .ok()
    }

    fn handle_channel_typing_tagmsg(
        &mut self,
        channel: &target::Channel,
        nick: NickRef<'_>,
        typing: Option<&str>,
    ) {
        match typing {
            Some("active") => self.update_channel_typing(channel, nick),
            Some("done") => self.clear_channel_typing(channel, nick),
            _ => {}
        }
    }

    fn handle_query_typing_tagmsg(
        &mut self,
        query: &target::Query,
        typing: Option<&str>,
    ) {
        match typing {
            Some("active") => self.update_query_typing(query),
            Some("done") => self.clear_query_typing(query),
            _ => {}
        }
    }

    pub fn nickname(&self) -> NickRef<'_> {
        // TODO: Fallback nicks
        self.resolved_nick
            .as_ref()
            .unwrap_or(&self.configured_nick)
            .as_nickref()
    }

    pub fn tick(&mut self, now: Instant) -> Result<()> {
        match self.notification_blackout {
            NotificationBlackout::Blackout(instant) => {
                if now.duration_since(instant) >= HIGHLIGHT_BLACKOUT_INTERVAL {
                    self.notification_blackout =
                        NotificationBlackout::Receiving;
                }
            }
            NotificationBlackout::Receiving => {}
        }

        self.chanmap
            .values_mut()
            .for_each(|channel| prune_expired_typing(&mut channel.typing));
        prune_expired_querymap(&mut self.querymap);

        if let Some(who_poll) = self.who_polls.front_mut() {
            #[derive(Debug)]
            enum Request {
                Poll,
                Retry,
            }

            let request = match &who_poll.status {
                WhoStatus::Joined => {
                    (self.capabilities.acknowledged(Capability::AwayNotify)
                        || self.config.who_poll_enabled)
                        .then_some(Request::Poll)
                }
                WhoStatus::Waiting(last) => {
                    if self.capabilities.acknowledged(Capability::AwayNotify) {
                        self.chanmap.get(&who_poll.channel).and_then(
                            |channel| {
                                (!channel.who_init
                                    && (now.duration_since(*last)
                                        >= self.who_poll_interval.duration()))
                                .then_some(Request::Poll)
                            },
                        )
                    } else {
                        (self.config.who_poll_enabled
                            && (now.duration_since(*last)
                                >= self.who_poll_interval.duration()))
                        .then_some(Request::Poll)
                    }
                }
                WhoStatus::Requested(source, requested, _) => {
                    if matches!(source, WhoSource::Poll)
                        && !self.config.who_poll_enabled
                    {
                        None
                    } else {
                        (now.duration_since(*requested)
                            >= 5 * self.who_poll_interval.duration())
                        .then_some(Request::Retry)
                    }
                }
                _ => None,
            };

            if let Some(request) = request {
                log::trace!(
                    "[{}] {} - WHO {}",
                    self.server,
                    who_poll.channel,
                    match request {
                        Request::Poll => "poll",
                        Request::Retry => "retry",
                    }
                );

                let message =
                    if self.isupport.contains_key(&isupport::Kind::WHOX) {
                        let whox_params = if self
                            .capabilities
                            .acknowledged(Capability::AccountNotify)
                        {
                            WhoXPollParameters::WithAccountName
                        } else {
                            WhoXPollParameters::Default
                        };

                        who_poll.status = WhoStatus::Requested(
                            WhoSource::Poll,
                            Instant::now(),
                            Some(whox_params.token()),
                        );

                        command!(
                            "WHO",
                            who_poll.channel.to_string(),
                            whox_params.fields().to_string(),
                            whox_params.token().to_owned()
                        )
                    } else {
                        who_poll.status = WhoStatus::Requested(
                            WhoSource::Poll,
                            Instant::now(),
                            None,
                        );

                        command!("WHO", who_poll.channel.to_string())
                    };

                self.send(None, message.into(), TokenPriority::Low);
            }
        }

        self.mode_requests.retain(|mode_request| {
            if let ModeStatus::Requested(instant)
            | ModeStatus::Received(instant) = mode_request.status
                && now.duration_since(instant) >= MODE_REQUEST_TIMEOUT
            {
                false
            } else {
                true
            }
        });

        let mut mode_requests = Vec::new();

        for mode_request in self.mode_requests.iter_mut() {
            if let ModeStatus::Joined(joined_at) = mode_request.status
                && now.duration_since(joined_at) > MODE_REQUEST_DELAY
            {
                mode_request.status = ModeStatus::Requested(Instant::now());
                mode_requests
                    .push(command!("MODE", mode_request.channel.to_string()));
            }
        }

        for mode_request in mode_requests {
            self.send(None, mode_request.into(), TokenPriority::Low);
        }

        // NOTE(pounce): change to `BinaryHeap::pop_if` when stabilized
        while let Some(sync) = self.metadata_syncs.peek_mut().and_then(|sync| {
            (sync.ready_at <= now).then_some(PeekMut::pop(sync))
        }) {
            log::debug!(
                "[{}] Sending METADATA SYNC for [{}]",
                self.server,
                sync.target
            );
            self.send(
                None,
                command!("METADATA", sync.target.to_string(), "SYNC").into(),
                TokenPriority::Low,
            );
        }

        self.chathistory_requests.retain(|_, chathistory_request| {
            now.duration_since(chathistory_request.requested_at)
                < CHATHISTORY_REQUEST_TIMEOUT
        });

        if let Some(ref mut anti_flood) = self.anti_flood {
            anti_flood.add_permits(now.into());

            for message in anti_flood.acquire_tokens() {
                if let Err(e) = self.handle.try_send(message.into()) {
                    log::warn!("[{}] Error sending message: {e}", self.server);
                }
            }
        }

        Ok(())
    }

    pub fn casemapping(&self) -> isupport::CaseMap {
        isupport::get_casemapping_or_default(&self.isupport)
    }

    pub fn chanmodes(&self) -> &[isupport::ModeKind] {
        isupport::get_chanmodes_or_default(&self.isupport)
    }

    pub fn chantypes(&self) -> &[char] {
        isupport::get_chantypes_or_default(&self.isupport)
    }

    pub fn prefix(&self) -> &[isupport::PrefixMap] {
        isupport::get_prefix_or_default(&self.isupport)
    }

    pub fn statusmsg(&self) -> &[char] {
        isupport::get_statusmsg_or_default(&self.isupport)
    }

    pub fn filehost(&self) -> Option<&str> {
        if !self.config.filehost.enabled {
            return None;
        }

        self.config
            .filehost
            .override_url
            .as_deref()
            .or(isupport::get_filehost(&self.isupport))
    }

    pub fn safelist(&self) -> bool {
        self.isupport.contains_key(&isupport::Kind::SAFELIST)
    }

    fn supports_typing(&self) -> bool {
        self.capabilities.acknowledged(Capability::MessageTags)
    }

    fn can_send_typing(&self) -> bool {
        self.capabilities.acknowledged(Capability::MessageTags)
            && isupport::is_client_tag_allowed(&self.isupport, "typing")
    }

    fn show_typing(&self) -> bool {
        self.supports_typing()
            && self.config.typing.show.is_some_and(|show| show)
    }

    fn share_typing(&self) -> bool {
        self.can_send_typing()
            && self.config.typing.share.is_some_and(|share| share)
    }

    fn can_send_reactions(&self) -> bool {
        self.can_send_replies()
            && isupport::is_client_tag_allowed(&self.isupport, "draft/react")
            && isupport::is_client_tag_allowed(&self.isupport, "draft/unreact")
    }

    fn can_send_replies(&self) -> bool {
        self.capabilities.acknowledged(Capability::MessageTags)
            && (isupport::is_client_tag_allowed(&self.isupport, "draft/reply")
                || isupport::is_client_tag_allowed(&self.isupport, "reply"))
    }

    fn can_redact(&self) -> bool {
        self.capabilities.acknowledged(Capability::MessageTags)
            && self.capabilities.acknowledged(Capability::MessageRedaction)
    }

    pub fn is_channel(&self, target: &str) -> bool {
        proto::is_channel(target, self.chantypes())
    }

    // Known bytes or a reasonable, conservative estimate of
    // ':nick!user@host ' that must be prepended to relay a
    // PRIVMSG/NOTICE, plus a safety buffer of 10 bytes
    pub fn relay_bytes(&self) -> usize {
        self.nickname().as_str().len()
            + if let Some(resolved_user) = &self.resolved_user {
                resolved_user.len()
            } else if let Some(isupport::Parameter::USERLEN(max_len)) =
                self.isupport.get(&isupport::Kind::USERLEN)
            {
                *max_len as usize
            } else {
                32
            }
            + if let Some(resolved_host) = &self.resolved_host {
                resolved_host.len()
            } else if let Some(isupport::Parameter::HOSTLEN(max_len)) =
                self.isupport.get(&isupport::Kind::HOSTLEN)
            {
                *max_len as usize
            } else {
                64
            }
            + 14
    }

    pub fn multiline_limits(&self) -> Option<MultilineLimits> {
        self.capabilities.multiline_limits()
    }

    pub fn has_isupport_monitor(&self) -> bool {
        self.isupport.contains_key(&isupport::Kind::MONITOR)
    }

    pub fn is_monitored_user(&self, user: &User) -> bool {
        self.monitored_users.contains_key(user)
    }

    pub fn is_monitored_user_online(&self, user: &User) -> bool {
        // falling back to assume nick is `online` if we don't have monitor support
        !self.has_isupport_monitor()
            || self
                .monitored_users
                .get(user)
                .is_some_and(|monitored_user| monitored_user.online)
    }

    pub fn is_monitored_user_automated(&self, user: &User) -> bool {
        self.monitored_users
            .get(user)
            .is_some_and(|monitored_user| monitored_user.automated)
    }

    pub fn add_monitored_user_automated(&mut self, user: &User) {
        if self.has_isupport_monitor() {
            self.monitored_users.insert(
                user.clone(),
                MonitoredUser {
                    online: self.is_monitored_user_online(user),
                    automated: true,
                },
            );
            self.send(
                None,
                command!("MONITOR", "+", user.as_normalized_str()).into(),
                TokenPriority::Low,
            );
        }
    }

    pub fn remove_monitored_user(&mut self, user: &User) {
        if self.has_isupport_monitor() {
            self.monitored_users.remove(user);
            self.send(
                None,
                command!("MONITOR", "-", user.as_normalized_str()).into(),
                TokenPriority::Low,
            );
        }
    }
}

fn compare_channels_default(chantypes: &[char], a: &str, b: &str) -> Ordering {
    let (Some(a_chantype), Some(b_chantype)) =
        (a.chars().next(), b.chars().next())
    else {
        return a.cmp(b);
    };

    if [a_chantype, b_chantype]
        .iter()
        .all(|c| chantypes.contains(c))
    {
        let ord = a
            .trim_start_matches(a_chantype)
            .cmp(b.trim_start_matches(b_chantype));
        if ord != Ordering::Equal {
            return ord;
        }
    }

    a.cmp(b)
}

fn compare_channels(
    config: &config::server::Server,
    chantypes: &[char],
    casemapping: isupport::CaseMap,
    a: &str,
    b: &str,
) -> Ordering {
    match config.order_channels_by {
        /*
         * If config.sidebar.order_channels_by is `name` (default), this will sort channels together
         * which have similar names when the chantype prefix (sometimes multiplied) is removed.
         *   e.g., '#chat', '##chat-offtopic' and '&chat-local' all get sorted together instead of
         *   in wildly different places.
         */
        None => compare_channels_default(chantypes, a, b),
        Some(config::sidebar::OrderChannelsBy::Name) => {
            compare_channels_default(chantypes, a, b)
        }
        /*
         * If config.sidebar.order_channels_by is `name-and-prefix`, this will sort channels
         * alphabetically by their full name including the prefix symbols. e.g., '#chat' and '##chat'
         * will be in different sections of the list.
         */
        Some(config::sidebar::OrderChannelsBy::NameAndPrefix) => a.cmp(b),
        /*
         * If config.sidebar.order_channels_by is `config`, this will sort channels based on your
         * config.server.<server_name>.channels. Anything not in this list is sorted by `name` (default).
         */
        Some(config::sidebar::OrderChannelsBy::Config) => {
            let a_pos = &config
                .channels
                .iter()
                .position(|ch| casemapping.normalize(ch) == a);
            let b_pos = &config
                .channels
                .iter()
                .position(|ch| casemapping.normalize(ch) == b);
            match (a_pos, b_pos) {
                (Some(a_pos), Some(b_pos)) => a_pos.cmp(b_pos),
                (Some(_), None) => Ordering::Less,
                (None, Some(_)) => Ordering::Greater,
                (None, None) => compare_channels_default(chantypes, a, b),
            }
        }
    }
}

fn is_reaction(message: &message::Encoded) -> bool {
    matches!(message.command, Command::PRIVMSG(..) | Command::TAGMSG(..))
        && message.in_reply_to().is_some()
        && (message.tags.contains_key("+draft/react")
            || message.tags.contains_key("+draft/unreact"))
}

fn continue_chathistory_between(
    target: &Target,
    events: &[Event],
    previous_start_message_reference: Option<&MessageReference>,
    end_message_reference: &MessageReference,
    limit: u16,
) -> Option<ChatHistorySubcommand> {
    let start_message_reference =
        events.first().and_then(|first_event| match first_event {
            Event::Single { message, .. }
            | Event::PrivOrNotice { message, .. }
            | Event::WithTarget { message, .. }
            | Event::DirectMessage(message, _, _)
            | Event::Reaction { message, .. }
            | Event::Redaction(message, _) => match end_message_reference {
                MessageReference::MessageId(_) => {
                    message.message_id().map(MessageReference::MessageId)
                }
                MessageReference::Timestamp(end_server_time) => {
                    let server_time = message.server_time();

                    let previous_server_time = if let Some(
                        MessageReference::Timestamp(previous_start_timestamp),
                    ) =
                        previous_start_message_reference
                    {
                        Some(previous_start_timestamp)
                    } else {
                        None
                    };

                    if let Some(server_time) = server_time
                        && previous_server_time.is_none_or(
                            |previous_server_time| {
                                server_time < *previous_server_time
                            },
                        )
                        && server_time > *end_server_time
                    {
                        Some(MessageReference::Timestamp(server_time))
                    } else {
                        None
                    }
                }
            },
            Event::Broadcast(_)
            | Event::FileTransferRequest(_)
            | Event::UpdateReadMarker(_, _)
            | Event::JoinedChannel(_, _)
            | Event::LoggedIn(_)
            | Event::AddedIsupportParam(_)
            | Event::ChatHistoryTargetReceived(_, _)
            | Event::ChatHistoryTargetsReceived(_)
            | Event::MonitoredOnline(_)
            | Event::MonitoredOffline(_)
            | Event::OnConnect(_)
            | Event::BouncerNetwork(_, _)
            | Event::AddToSidebar(_)
            | Event::Disconnect(_)
            | Event::UpdateIcon => None,
        });

    start_message_reference.map(|start_message_reference| {
        ChatHistorySubcommand::Between(
            target.clone(),
            start_message_reference,
            end_message_reference.clone(),
            limit,
        )
    })
}

fn continue_chathistory_targets(
    events: &[Event],
    previous_start_timestamp: &DateTime<Utc>,
    end_timestamp: &DateTime<Utc>,
    limit: u16,
) -> Option<ChatHistorySubcommand> {
    let start_timestamp =
        events.last().and_then(|first_event| match first_event {
            Event::ChatHistoryTargetReceived(_, server_time) => {
                if server_time > previous_start_timestamp
                    && server_time < end_timestamp
                {
                    Some(*server_time)
                } else {
                    None
                }
            }
            Event::Single { .. }
            | Event::PrivOrNotice { .. }
            | Event::WithTarget { .. }
            | Event::DirectMessage(_, _, _)
            | Event::Reaction { .. }
            | Event::Redaction(_, _)
            | Event::Broadcast(_)
            | Event::FileTransferRequest(_)
            | Event::UpdateReadMarker(_, _)
            | Event::JoinedChannel(_, _)
            | Event::LoggedIn(_)
            | Event::AddedIsupportParam(_)
            | Event::ChatHistoryTargetsReceived(_)
            | Event::MonitoredOnline(_)
            | Event::MonitoredOffline(_)
            | Event::OnConnect(_)
            | Event::BouncerNetwork(_, _)
            | Event::AddToSidebar(_)
            | Event::Disconnect(_)
            | Event::UpdateIcon => None,
        });

    start_timestamp.map(|start_timestamp| {
        ChatHistorySubcommand::Targets(start_timestamp, *end_timestamp, limit)
    })
}

async fn chathistory_targets_path(server: &Server) -> Result<PathBuf, Error> {
    let data_dir = environment::data_dir();

    let targets_dir = data_dir.join("targets");

    if !targets_dir.exists() {
        fs::create_dir_all(&targets_dir).await?;
    }

    let hashed_server = seahash::hash(format!("{server}").as_bytes());

    Ok(targets_dir.join(format!("{hashed_server}.json")))
}

pub async fn load_chathistory_targets_timestamp(
    server: Server,
) -> Result<Option<DateTime<Utc>>, Error> {
    let path = chathistory_targets_path(&server).await?;

    if let Ok(bytes) = fs::read(path).await {
        Ok(serde_json::from_slice(&bytes).unwrap_or_default())
    } else {
        Ok(None)
    }
}

pub async fn overwrite_chathistory_targets_timestamp(
    server: Server,
    timestamp: DateTime<Utc>,
) -> Result<(), Error> {
    let bytes = serde_json::to_vec(&Some(timestamp))?;

    let path = chathistory_targets_path(&server).await?;

    fs::write(path, &bytes).await?;

    Ok(())
}

#[derive(Debug)]
enum NotificationBlackout {
    Blackout(Instant),
    Receiving,
}

impl NotificationBlackout {
    fn allowed(&self) -> bool {
        match self {
            NotificationBlackout::Blackout(_) => false,
            NotificationBlackout::Receiving => true,
        }
    }
}

#[derive(Debug, Default)]
pub struct Map(BTreeMap<Server, State>);

impl Map {
    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn disconnected(&mut self, server: Server) {
        self.0.insert(server, State::Disconnected);
    }

    pub fn ready(&mut self, server: Server, client: Client) {
        self.0.insert(server, State::Ready(client));
    }

    pub fn update_config(
        &mut self,
        server: &Server,
        config: Arc<config::Server>,
        from_modal: bool,
    ) -> Vec<Event> {
        if let Some(State::Ready(client)) = self.0.get_mut(server) {
            client.update_config(config, from_modal)
        } else {
            vec![]
        }
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn remove(&mut self, server: &Server) -> Option<Client> {
        self.0.remove(server).and_then(|state| match state {
            State::Disconnected => None,
            State::Ready(client) => Some(client),
        })
    }

    pub fn client(&self, server: &Server) -> Option<&Client> {
        if let Some(State::Ready(client)) = self.0.get(server) {
            Some(client)
        } else {
            None
        }
    }

    pub fn client_mut(&mut self, server: &Server) -> Option<&mut Client> {
        if let Some(State::Ready(client)) = self.0.get_mut(server) {
            Some(client)
        } else {
            None
        }
    }

    pub fn nickname<'a>(&'a self, server: &Server) -> Option<NickRef<'a>> {
        self.client(server).map(Client::nickname)
    }

    pub fn receive(
        &mut self,
        server: &Server,
        message: message::Encoded,
        config: &config::Config,
    ) -> Result<Vec<Event>> {
        if let Some(client) = self.client_mut(server) {
            client.receive(message, config)
        } else {
            Ok(Vec::default())
        }
    }

    pub fn send(
        &mut self,
        buffer: &buffer::Upstream,
        message: message::Encoded,
        priority: TokenPriority,
    ) -> Option<LabeledResponseContext> {
        self.client_mut(buffer.server())
            .and_then(|client| client.send(Some(buffer), message, priority))
    }

    pub fn send_multiline_batch(
        &mut self,
        buffer: &buffer::Upstream,
        messages: Vec<message::Encoded>,
        priority: TokenPriority,
        reply_id: Option<&message::Id>,
    ) -> Option<LabeledResponseContext> {
        self.client_mut(buffer.server()).and_then(|client| {
            client.send_multiline_batch(buffer, messages, priority, reply_id)
        })
    }

    pub fn send_markread(
        &mut self,
        server: &Server,
        target: Target,
        read_marker: ReadMarker,
        priority: TokenPriority,
    ) {
        if let Some(client) = self.client_mut(server) {
            client.send_markread(target, read_marker, priority);
        }
    }

    pub fn join(&mut self, server: &Server, channels: &[target::Channel]) {
        if let Some(client) = self.client_mut(server) {
            client.join(channels);
        }
    }

    pub fn quit(&mut self, server: &Server, reason: Option<String>) {
        if let Some(client) = self.client_mut(server) {
            client.quit(reason);
        }
    }

    pub fn resolve_user_attributes<'a>(
        &'a self,
        server: &Server,
        channel: &target::Channel,
        user: &User,
    ) -> Option<&'a User> {
        self.client(server)
            .and_then(|client| client.resolve_user_attributes(channel, user))
    }

    pub fn get_channel_discovery_manager(
        &self,
        server: &Server,
    ) -> Option<&channel_discovery::Manager> {
        self.client(server)
            .map(|client| &client.channel_discovery_manager)
    }

    pub fn get_channel_discovery_manager_mut(
        &mut self,
        server: &Server,
    ) -> Option<&mut channel_discovery::Manager> {
        self.client_mut(server)
            .map(|client| &mut client.channel_discovery_manager)
    }

    pub fn get_channel_users(
        &self,
        server: &Server,
        channel: &target::Channel,
    ) -> Option<&ChannelUsers> {
        self.client(server).and_then(|client| client.users(channel))
    }

    pub fn get_user_channels(
        &self,
        server: &Server,
        nick: NickRef,
    ) -> Vec<target::Channel> {
        self.client(server)
            .map(|client| client.user_channels(nick))
            .unwrap_or_default()
    }

    pub fn get_channel_topic<'a>(
        &'a self,
        server: &Server,
        channel: &target::Channel,
    ) -> Option<&'a Topic> {
        self.client(server)
            .map(|client| client.topic(channel))
            .unwrap_or_default()
    }

    pub fn get_channel_mode<'a>(
        &'a self,
        server: &Server,
        channel: &target::Channel,
    ) -> Option<&'a String> {
        self.client(server)
            .map(|client| client.mode(channel))
            .unwrap_or_default()
    }

    pub fn get_channels<'a>(
        &'a self,
        server: &Server,
    ) -> impl Iterator<Item = &'a target::Channel> {
        self.client(server)
            .map(Client::channels)
            .into_iter()
            .flatten()
    }

    pub fn contains_channel(
        &self,
        server: &Server,
        chan: &target::Channel,
    ) -> bool {
        self.client(server)
            .is_some_and(|c| c.chanmap.contains_key(chan))
    }

    pub fn resolve_query<'a>(
        &'a self,
        server: &Server,
        query: &target::Query,
    ) -> Option<&'a target::Query> {
        self.client(server)
            .and_then(|client| client.resolve_query(query))
    }

    pub fn get_isupport_ref(
        &self,
        server: &Server,
    ) -> &HashMap<isupport::Kind, isupport::Parameter> {
        self.client(server)
            .map_or(&isupport::DEFAULT, |client| &client.isupport)
    }

    pub fn get_capabilities_ref(&self, server: &Server) -> &Capabilities {
        self.client(server)
            .map_or(&capabilities::DEFAULT, |client| &client.capabilities)
    }

    pub fn get_filehost<'a>(&'a self, server: &Server) -> Option<&'a str> {
        let client = self.client(server)?;

        if !client.config.filehost.enabled {
            return None;
        }

        client.config.filehost.override_url.as_deref().or(
            if server.is_bouncer_network() {
                server.parent().as_ref().and_then(|p| self.get_filehost(p))
            } else {
                isupport::get_filehost(&client.isupport)
            },
        )
    }

    pub fn get_icon_url<'a>(&'a self, server: &Server) -> Option<&'a str> {
        self.client(server).and_then(|client| {
            if client.config.icon.enabled {
                client
                    .config
                    .icon
                    .override_url
                    .as_deref()
                    .or(isupport::get_icon_url(&client.isupport))
            } else {
                None
            }
        })
    }

    pub fn get_filehost_auth(
        &self,
        server: &Server,
    ) -> Option<fileupload::Auth> {
        let client = self.client(server)?;

        if !client.config.filehost.enabled {
            return None;
        }

        match &client.config.filehost.credentials {
            filehost::Credentials::Server => {
                if server.is_bouncer_network() {
                    return server
                        .parent()
                        .as_ref()
                        .and_then(|parent| self.get_filehost_auth(parent));
                }

                fileupload::Auth::from_sasl(
                    client.config.sasl.as_ref()?,
                    &client.config.nickname,
                )
                .ok()
            }
            filehost::Credentials::Sasl(credentials) => {
                fileupload::Auth::from_sasl(
                    credentials,
                    &client.config.nickname,
                )
                .ok()
            }
            filehost::Credentials::None => None,
        }
    }

    pub fn get_use_tls(&self, server: &Server) -> bool {
        self.client(server).is_none_or(|c| c.config.use_tls)
    }

    pub fn get_filehost_is_override(&self, server: &Server) -> bool {
        let Some(client) = self.client(server) else {
            return false;
        };

        if !client.config.filehost.enabled {
            return false;
        }

        if client.config.filehost.override_url.is_some() {
            return true;
        }

        if server.is_bouncer_network() {
            server
                .parent()
                .as_ref()
                .is_some_and(|p| self.get_filehost_is_override(p))
        } else {
            false
        }
    }

    pub fn get_features_ref(&self, server: &Server) -> &Features {
        self.client(server)
            .map_or(&features::DEFAULT, |client| &client.features)
    }

    pub fn get_server_casemapping_or_default(
        &self,
        server: &Server,
    ) -> isupport::CaseMap {
        self.get_maybe_server_casemapping_or_default(Some(server))
    }

    pub fn get_maybe_server_casemapping_or_default(
        &self,
        server: Option<&Server>,
    ) -> isupport::CaseMap {
        server
            .and_then(|server| self.client(server).map(Client::casemapping))
            .unwrap_or_default()
    }

    pub fn get_server_chanmodes_or_default<'a>(
        &'a self,
        server: &Server,
    ) -> &'a [isupport::ModeKind] {
        self.client(server)
            .map(Client::chanmodes)
            .unwrap_or_default()
    }

    pub fn get_server_chantypes_or_default<'a>(
        &'a self,
        server: &Server,
    ) -> &'a [char] {
        self.get_maybe_server_chantypes_or_default(Some(server))
    }

    pub fn get_maybe_server_chantypes_or_default<'a>(
        &'a self,
        server: Option<&Server>,
    ) -> &'a [char] {
        server
            .and_then(|server| self.client(server).map(Client::chantypes))
            .unwrap_or(isupport::DEFAULT_CHANTYPES)
    }

    pub fn get_server_prefix_or_default<'a>(
        &'a self,
        server: &Server,
    ) -> &'a [isupport::PrefixMap] {
        self.client(server).map(Client::prefix).unwrap_or_default()
    }

    pub fn get_server_statusmsg_or_default<'a>(
        &'a self,
        server: &Server,
    ) -> &'a [char] {
        self.get_maybe_server_statusmsg_or_default(Some(server))
    }

    pub fn get_maybe_server_statusmsg_or_default<'a>(
        &'a self,
        server: Option<&Server>,
    ) -> &'a [char] {
        server
            .and_then(|server| self.client(server).map(Client::statusmsg))
            .unwrap_or(isupport::DEFAULT_STATUSMSG)
    }

    // The default value is chosen to be a reasonable, conservative
    // estimate when no client is available
    pub fn get_relay_bytes(&self, server: &Server) -> usize {
        self.client(server).map_or(144, Client::relay_bytes)
    }

    pub fn get_multiline_limits(
        &self,
        server: &Server,
    ) -> Option<MultilineLimits> {
        self.client(server).and_then(Client::multiline_limits)
    }

    pub fn get_server_supports_multiline(&self, server: &Server) -> bool {
        self.client(server).is_some_and(|client| {
            client.capabilities.acknowledged(Capability::Multiline)
        })
    }

    pub fn get_server_supports_echoes(&self, server: &Server) -> bool {
        self.client(server).is_some_and(|client| {
            client.capabilities.acknowledged(Capability::EchoMessage)
        })
    }

    pub fn get_server_supports_typing(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::supports_typing)
    }

    pub fn get_server_can_send_reactions(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::can_send_reactions)
    }

    pub fn get_server_can_redact(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::can_redact)
    }

    pub fn get_server_can_send_replies(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::can_send_replies)
    }

    pub fn get_server_can_send_typing(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::can_send_typing)
    }

    pub fn get_server_show_typing(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::show_typing)
    }

    pub fn get_server_share_typing(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::share_typing)
    }

    pub fn get_channel_typing_users(
        &self,
        server: &Server,
        channel: &target::Channel,
    ) -> Vec<String> {
        self.client(server)
            .map(|client| client.channel_typing_users(channel))
            .unwrap_or_default()
    }

    pub fn get_query_typing_users(
        &self,
        server: &Server,
        query: &target::Query,
    ) -> Vec<String> {
        self.client(server)
            .map(|client| client.query_typing_users(query))
            .unwrap_or_default()
    }

    pub fn has_channel_typing_users(
        &self,
        server: &Server,
        channel: &target::Channel,
    ) -> bool {
        self.client(server)
            .is_some_and(|client| client.has_channel_typing_users(channel))
    }

    pub fn has_query_typing_users(
        &self,
        server: &Server,
        query: &target::Query,
    ) -> bool {
        self.client(server)
            .is_some_and(|client| client.has_query_typing_users(query))
    }

    pub fn get_server_chathistory_message_reference_types(
        &self,
        server: &Server,
    ) -> Vec<isupport::MessageReferenceType> {
        self.client(server)
            .map(Client::chathistory_message_reference_types)
            .unwrap_or_default()
    }

    pub fn get_server_chathistory_limit(&self, server: &Server) -> u16 {
        self.client(server)
            .map_or(CLIENT_CHATHISTORY_LIMIT, |client| {
                client.chathistory_limit()
            })
    }

    pub fn get_server_supports_chathistory(&self, server: &Server) -> bool {
        self.client(server).is_some_and(|client| {
            client.capabilities.acknowledged(Capability::Chathistory)
        })
    }

    pub fn get_chathistory_request(
        &self,
        server: &Server,
        target: &Target,
    ) -> Option<ChatHistorySubcommand> {
        self.client(server)
            .and_then(|client| client.chathistory_request(target))
    }

    pub fn send_chathistory_request(
        &mut self,
        server: &Server,
        subcommand: ChatHistorySubcommand,
        priority: TokenPriority,
    ) {
        if let Some(client) = self.client_mut(server) {
            client.send_chathistory_request(subcommand, priority);
        }
    }

    pub fn clear_chathistory_request(
        &mut self,
        server: &Server,
        target: Option<&Target>,
    ) {
        if let Some(client) = self.client_mut(server) {
            client.clear_chathistory_request(target);
        }
    }

    pub fn get_chathistory_exhausted(
        &self,
        server: &Server,
        target: &Target,
    ) -> bool {
        self.client(server)
            .is_some_and(|client| client.chathistory_exhausted(target))
    }

    pub fn get_chathistory_state(
        &self,
        server: &Server,
        target: &Target,
    ) -> Option<ChatHistoryState> {
        self.client(server).and_then(|client| {
            if client.capabilities.acknowledged(Capability::Chathistory) {
                if client.chathistory_request(target).is_some() {
                    Some(ChatHistoryState::PendingRequest)
                } else if client.chathistory_exhausted(target) {
                    Some(ChatHistoryState::Exhausted)
                } else {
                    Some(ChatHistoryState::Ready)
                }
            } else {
                None
            }
        })
    }

    pub fn load_chathistory_targets_timestamp(
        &self,
        server: &Server,
        server_time: DateTime<Utc>,
    ) -> Option<impl Future<Output = Message> + use<>> {
        self.client(server).map(|client| {
            client.load_chathistory_targets_timestamp(server_time)
        })
    }

    pub fn overwrite_chathistory_targets_timestamp(
        &self,
        server: &Server,
        server_time: DateTime<Utc>,
    ) -> Option<impl Future<Output = Message> + use<>> {
        self.client(server).map(|client| {
            client.overwrite_chathistory_targets_timestamp(server_time)
        })
    }

    pub fn get_server_supports_detach(&self, server: &Server) -> bool {
        self.client(server)
            .is_some_and(|client| client.features.detach)
    }

    pub fn get_server_supports_list(&self, server: &Server) -> bool {
        self.client(server).is_some_and(Client::safelist)
    }

    pub fn get_server_is_connected(&self, server: &Server) -> bool {
        self.client(server).is_some()
    }

    pub fn get_server_http_client(
        &self,
        server: &Server,
    ) -> Option<Arc<reqwest::Client>> {
        self.client(server)
            .and_then(|client| client.http_client.clone())
    }

    pub fn get_server_proxy_config(
        &self,
        server: &Server,
    ) -> Option<&config::Proxy> {
        self.client(server)
            .as_ref()
            .and_then(|client| client.config.proxy.as_ref())
    }

    pub fn get_seed(&self, kind: &history::Kind) -> Option<history::Seed> {
        match kind {
            history::Kind::Highlights => {
                let casemappings: HashMap<Server, isupport::CaseMap> = self
                    .servers()
                    .filter_map(|server| {
                        self.client(server)
                            .map(Client::casemapping)
                            .map(|casemapping| (server.clone(), casemapping))
                    })
                    .collect();

                (!casemappings.is_empty())
                    .then_some(history::Seed::Multiple(casemappings))
            }
            _ => kind.server().and_then(|server| {
                self.client(server)
                    .map(Client::casemapping)
                    .map(history::Seed::Single)
            }),
        }
    }

    pub fn get_server_handle(
        &self,
        server: &Server,
    ) -> Option<&server::Handle> {
        self.client(server).map(|client| &client.handle)
    }

    pub fn connected_servers(&self) -> impl Iterator<Item = &Server> {
        self.0.iter().filter_map(|(server, state)| {
            if let State::Ready(_) = state {
                Some(server)
            } else {
                None
            }
        })
    }

    pub fn servers(&self) -> impl Iterator<Item = &Server> {
        self.0.keys()
    }

    pub fn iter(&self) -> std::collections::btree_map::Iter<'_, Server, State> {
        self.0.iter()
    }

    pub fn status(&self, server: &Server) -> Status {
        self.0.get(server).map_or(Status::Unavailable, |s| match s {
            State::Disconnected => Status::Disconnected,
            State::Ready(_) => Status::Connected,
        })
    }

    pub fn state(&self, server: &Server) -> Option<&State> {
        self.0.get(server)
    }

    pub fn tick(&mut self, now: Instant) -> Result<()> {
        for client in self.0.values_mut() {
            if let State::Ready(client) = client {
                client.tick(now).with_context(|| {
                    anyhow!("[{}] tick failed", client.server)
                })?;
            }
        }
        Ok(())
    }

    pub fn get_registry(&self, server: &Server) -> &dyn metadata::Registry {
        self.0
            .get(server)
            .and_then::<&dyn metadata::Registry, _>(|state| match state {
                State::Disconnected => None,
                State::Ready(client) => Some(&client.registry),
            })
            .unwrap_or(metadata::EMPTY)
    }
}

#[derive(Debug, Clone)]
pub enum Context {
    Buffer(buffer::Upstream),
    PrivOrNotice(buffer::Upstream, Option<LabeledResponseContext>),
}

impl Context {
    fn new(
        message: &message::Encoded,
        buffer: buffer::Upstream,
        label: Option<&str>,
    ) -> Self {
        match &message.command {
            Command::PRIVMSG(_, _) | Command::NOTICE(_, _) => {
                Self::PrivOrNotice(
                    buffer,
                    label.map(|label| {
                        LabeledResponseContext::new(message, label)
                    }),
                )
            }
            Command::BATCH(_, params)
                if params.iter().any(|param| param == "draft/multiline") =>
            {
                Self::PrivOrNotice(
                    buffer,
                    label.map(|label| {
                        LabeledResponseContext::new(message, label)
                    }),
                )
            }
            _ => Self::Buffer(buffer),
        }
    }

    fn buffer(self) -> buffer::Upstream {
        match self {
            Context::Buffer(buffer) => buffer,
            Context::PrivOrNotice(buffer, _) => buffer,
        }
    }

    fn labeled_response_context(&self) -> Option<&LabeledResponseContext> {
        match self {
            Context::PrivOrNotice(_, labeled_response_context) => {
                labeled_response_context.as_ref()
            }
            Context::Buffer(_) => None,
        }
    }
}

impl From<Context> for Option<LabeledResponseContext> {
    fn from(context: Context) -> Option<LabeledResponseContext> {
        match context {
            Context::PrivOrNotice(_, labeled_response_context) => {
                labeled_response_context
            }
            Context::Buffer(_) => None,
        }
    }
}

#[derive(Debug)]
pub enum BatchKind {
    ChathistoryTarget(Target),
    ChathistoryTargets,
    Multiline(
        Tags,
        Option<User>,
        Target,
        Option<MultilineBatchKind>,
        String,
    ),
    ZncPlayback(Target),
}

impl BatchKind {
    pub fn target(&self) -> Option<Target> {
        match self {
            Self::ChathistoryTarget(batch_target)
            | Self::Multiline(_, _, batch_target, _, _)
            | Self::ZncPlayback(batch_target) => Some(batch_target.clone()),
            Self::ChathistoryTargets => None,
        }
    }
}

#[derive(Debug)]
pub struct Batch {
    context: Option<Context>,
    events: Vec<Event>,
    kind: Option<BatchKind>,
    size: u16,
    chathistory_end: bool,
}

impl Batch {
    fn new(context: Option<Context>) -> Self {
        Self {
            context,
            events: vec![],
            kind: None,
            size: 0,
            chathistory_end: false,
        }
    }
}

fn generate_label() -> String {
    Posix::now().as_nanos().to_string()
}

fn generate_batch_reference_tag() -> String {
    Posix::now().as_nanos().to_string()
}

fn prune_expired_typing(typing: &mut HashMap<Nick, Instant>) {
    typing.retain(|_, updated_at| !is_typing_expired(*updated_at));
}

fn prune_expired_querymap(querymap: &mut IndexMap<target::Query, QueryState>) {
    querymap.retain(|_, query_state| {
        query_state.prune();
        !query_state.is_empty()
    });
}

fn is_typing_expired(updated_at: Instant) -> bool {
    updated_at.elapsed() >= TYPING_TIMEOUT
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum RegistrationStep {
    Start,
    List,
    Req,
    Sasl,
    End,
    Complete,
}

#[derive(Debug, Default)]
pub struct Channel {
    pub users: ChannelUsers,
    pub topic: Topic,
    pub names_init: bool,
    pub who_init: bool,
    pub mode: Option<String>,
    pub typing: HashMap<Nick, Instant>,
}

impl Channel {
    pub fn update_user_status(
        &mut self,
        user: &str,
        flags: &str,
        casemapping: isupport::CaseMap,
        bot_mode_char: Option<char>,
    ) {
        let user = User::from(Nick::from_str(user, casemapping));

        let away = flags.chars().next().and_then(|away_flag| {
            // H = Here, G = gone (away)
            match away_flag {
                'G' => Some(true),
                'H' => Some(false),
                _ => None,
            }
        });

        let bot = bot_mode_char.map(|bot_char| flags.contains(bot_char));

        self.users.update_user(&user, away, bot);
    }

    pub fn update_user_accountname(
        &mut self,
        user: &str,
        accountname: &str,
        casemapping: isupport::CaseMap,
    ) {
        let user = User::from(Nick::from_str(user, casemapping));

        if let Some(user) = self.users.take(&user) {
            self.users.insert(user.with_accountname(accountname));
        }
    }
}

#[derive(Debug, Default)]
pub struct QueryState {
    pub query: Option<target::Query>,
    pub typing: Option<Instant>,
}

impl QueryState {
    fn prune(&mut self) {
        if self.typing.is_some_and(is_typing_expired) {
            self.typing = None;
        }
    }

    fn is_empty(&self) -> bool {
        self.query.is_none() && self.typing.is_none()
    }
}

#[derive(Default, Debug, Clone)]
pub struct Topic {
    pub content: Option<message::Content>,
    pub who: Option<User>,
    pub time: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone)]
pub struct WhoPoll {
    pub channel: target::Channel,
    pub status: WhoStatus,
}

#[derive(Debug, Clone)]
pub enum WhoStatus {
    Requested(WhoSource, Instant, Option<WhoToken>),
    Receiving(WhoSource, Option<WhoToken>),
    Received,
    Waiting(Instant),
    Joined,
}

#[derive(Debug, Clone)]
pub enum WhoSource {
    User,
    Poll,
}

#[derive(Debug, Clone)]
pub struct ModeRequest {
    pub channel: target::Channel,
    pub status: ModeStatus,
}

#[derive(Debug, Clone)]
pub enum ModeStatus {
    Joined(Instant),
    Requested(Instant),
    Received(Instant),
}

#[derive(Debug, Clone)]
pub struct MetadataSync {
    pub target: Target,
    pub ready_at: Instant,
}

// A reverse order on ready_at, so we get a min-heap
impl Ord for MetadataSync {
    fn cmp(&self, other: &Self) -> Ordering {
        other
            .ready_at
            .cmp(&self.ready_at)
            .then(self.target.cmp(&other.target))
    }
}

impl PartialOrd for MetadataSync {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl PartialEq for MetadataSync {
    fn eq(&self, other: &Self) -> bool {
        self.ready_at == other.ready_at && self.target == other.target
    }
}

impl Eq for MetadataSync {}

fn group_capability_requests<'a>(
    capabilities: &'a [&'a str],
) -> impl Iterator<Item = proto::Message> + 'a {
    const MAX_LEN: usize = proto::format::BYTE_LIMIT - b"CAP REQ :\r\n".len();

    capabilities
        .iter()
        .scan(0, |count, capability| {
            // Capability + a space
            *count += capability.len() + 1;

            let chunk = *count / MAX_LEN;

            Some((chunk, capability))
        })
        .into_group_map()
        .into_values()
        .map(|capabilities| {
            command!("CAP", "REQ", capabilities.into_iter().join(" "))
        })
}

/// Group channels together into as few JOIN messages as possible
fn group_joins<'a>(
    channels: &'a [target::Channel],
    keys: &'a HashMap<String, String>,
    target_limit: Option<u16>,
) -> impl Iterator<Item = proto::Message> + 'a {
    const MAX_LEN: usize = proto::format::BYTE_LIMIT - b"JOIN \r\n".len();

    let (without_keys, with_keys): (Vec<_>, Vec<_>) =
        channels.iter().partition_map(|channel| {
            keys.get(channel.as_str())
                .map_or(Either::Left(channel), |key| {
                    Either::Right((channel, key))
                })
        });

    let joins_without_keys = without_keys
        .into_iter()
        .scan((0, 0, 0), |(char_count, target_count, chunk), channel| {
            // Channel + a comma
            *char_count += channel.as_str().len() + 1;
            *target_count += 1;

            if *char_count > MAX_LEN
                || target_limit
                    .is_some_and(|target_limit| *target_count > target_limit)
            {
                *chunk += 1;

                *char_count = channel.as_str().len() + 1;
                *target_count = 1;
            }

            Some((*chunk, channel))
        })
        .into_group_map()
        .into_values()
        .map(|channels| command!("JOIN", channels.into_iter().join(",")));

    let joins_with_keys = with_keys
        .into_iter()
        .scan(
            (0, 0, 0),
            |(char_count, target_count, chunk), (channel, key)| {
                // Channel + key + a comma for each
                *char_count += channel.as_str().len() + key.len() + 2;
                *target_count += 1;

                if *char_count > MAX_LEN
                    || target_limit.is_some_and(|target_limit| {
                        *target_count > target_limit
                    })
                {
                    *chunk += 1;

                    *char_count = channel.as_str().len() + key.len() + 2;
                    *target_count = 1;
                }

                Some((*chunk, (channel, key)))
            },
        )
        .into_group_map()
        .into_values()
        .map(|values| {
            command!(
                "JOIN",
                values.iter().map(|(c, _)| c).join(","),
                values.iter().map(|(_, k)| k).join(",")
            )
        });

    joins_without_keys.chain(joins_with_keys)
}

fn group_monitors<'a>(
    users: &'a [String],
    monitor_limit: Option<u16>,
    target_limit: Option<u16>,
    server: &Server,
    num_existing_monitors: usize,
) -> impl Iterator<Item = proto::Message> + 'a {
    const MAX_LEN: usize = proto::format::BYTE_LIMIT - b"MONITOR + \r\n".len();

    if let Some(monitor_limit) = monitor_limit.map(usize::from) {
        let total_monitor_count = users.len() + num_existing_monitors;
        let remove_target_count = total_monitor_count.saturating_sub(monitor_limit);
        if remove_target_count > 0 {
            log::warn!(
                "[{server}] More users in monitor list than permitted by the server \
                      ({total_monitor_count} users in monitor list, {monitor_limit} permitted)",
            );
        }

        users.iter().take(users.len().saturating_sub(remove_target_count))
    } else {
        users.iter().take(users.len())
    }
    .scan((0, 0, 0), |(char_count, target_count, chunk), target| {
        // Target + a comma
        *char_count += target.len() + 1;
        *target_count += 1;

        if *char_count > MAX_LEN
            || target_limit
                .is_some_and(|target_limit| *target_count > target_limit)
        {
            *chunk += 1;

            *char_count = target.len() + 1;
            *target_count = 1;
        }

        Some((*chunk, target))
    })
    .into_group_map()
    .into_values()
    .map(|targets| command!("MONITOR", "+", targets.into_iter().join(",")))
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error(transparent)]
    Compression(#[from] compression::Error),
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
    SerdeJson(#[from] serde_json::Error),
    #[error(transparent)]
    Target(#[from] target::ParseError),
    #[error(transparent)]
    BouncerNetwork(#[from] bouncer::Error),
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use futures::channel::mpsc;
    use irc::proto;

    use super::*;

    fn test_client(nickname: &str) -> Client {
        let (sender, _) = mpsc::channel(1);

        Client::new(
            Server::from(Arc::<str>::from("test")),
            Arc::new(config::Server {
                nickname: nickname.to_string(),
                ..Default::default()
            }),
            sender,
        )
    }

    fn query(target: &str) -> target::Query {
        target::Query::parse(
            target,
            isupport::DEFAULT_CHANTYPES,
            &[],
            isupport::CaseMap::default(),
        )
        .unwrap()
    }

    #[test]
    fn resolve_query_uses_querymap_casing() {
        let mut client = test_client("tester");
        let canonical = query("FooBar");

        client.querymap.entry(canonical.clone()).or_default();

        assert_eq!(client.resolve_query(&query("foobar")), Some(&canonical));
    }

    #[test]
    fn chathistory_multiline_batch_assembles_into_single_message() {
        let mut client = test_client("tester");
        let config = config::Config::default();

        let alice = proto::Source::User(proto::User {
            nickname: "alice".to_string(),
            username: Some("alice".to_string()),
            hostname: Some("example.test".to_string()),
        });
        let server = proto::Source::Server("irc.test".to_string());

        // BATCH +1 chathistory #test
        client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::default(),
                    source: Some(server.clone()),
                    command: Command::BATCH(
                        "+1".to_string(),
                        vec!["chathistory".to_string(), "#test".to_string()],
                    ),
                }),
                None,
                &config,
            )
            .unwrap();

        // @batch=1 BATCH +2 draft/multiline #test
        client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::from([(
                        "batch".to_string(),
                        "1".to_string(),
                    )]),
                    source: Some(alice.clone()),
                    command: Command::BATCH(
                        "+2".to_string(),
                        vec![
                            "draft/multiline".to_string(),
                            "#test".to_string(),
                        ],
                    ),
                }),
                None,
                &config,
            )
            .unwrap();

        // @batch=2 PRIVMSG #test :line one
        client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::from([(
                        "batch".to_string(),
                        "2".to_string(),
                    )]),
                    source: Some(alice.clone()),
                    command: Command::PRIVMSG(
                        "#test".to_string(),
                        "line one".to_string(),
                    ),
                }),
                None,
                &config,
            )
            .unwrap();

        // @batch=2 PRIVMSG #test :line two
        client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::from([(
                        "batch".to_string(),
                        "2".to_string(),
                    )]),
                    source: Some(alice.clone()),
                    command: Command::PRIVMSG(
                        "#test".to_string(),
                        "line two".to_string(),
                    ),
                }),
                None,
                &config,
            )
            .unwrap();

        // @batch=1 BATCH -2
        client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::from([(
                        "batch".to_string(),
                        "1".to_string(),
                    )]),
                    source: Some(server.clone()),
                    command: Command::BATCH("-2".to_string(), vec![]),
                }),
                None,
                &config,
            )
            .unwrap();

        // BATCH -1
        let events = client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::default(),
                    source: Some(server.clone()),
                    command: Command::BATCH("-1".to_string(), vec![]),
                }),
                None,
                &config,
            )
            .unwrap();

        // The multiline message should appear as a single PrivOrNotice
        // with combined text "line one\nline two"
        let priv_events: Vec<_> = events
            .iter()
            .filter(|e| matches!(e, Event::PrivOrNotice { .. }))
            .collect();

        assert_eq!(
            priv_events.len(),
            1,
            "expected 1 assembled multiline message, got {}",
            priv_events.len()
        );

        if let Event::PrivOrNotice { message, .. } = &priv_events[0] {
            match &message.0.command {
                Command::PRIVMSG(_, text) => {
                    assert_eq!(
                        text, "line one\nline two",
                        "multiline text should be joined with newline"
                    );
                }
                other => panic!("expected PRIVMSG, got {other:?}"),
            }
        } else {
            panic!("expected PrivOrNotice event");
        }
    }

    #[test]
    fn chathistory_end_tag_marks_history_exhausted() {
        let mut client = test_client("tester");
        let config = config::Config::default();
        let server = proto::Source::Server("irc.test".to_string());
        let alice = proto::Source::User(proto::User {
            nickname: "alice".to_string(),
            username: Some("alice".to_string()),
            hostname: Some("example.test".to_string()),
        });

        let target = Target::parse(
            "#test",
            isupport::DEFAULT_CHANTYPES,
            &[],
            isupport::CaseMap::default(),
        );

        // limit=5 so that without draft/chathistory-end, receiving exactly 5
        // messages would leave exhausted=false (size >= limit)
        let limit: u16 = 5;

        client.chathistory_requests.insert(
            target.clone(),
            ChatHistoryRequest {
                subcommand: ChatHistorySubcommand::Latest(
                    target.clone(),
                    None,
                    limit,
                ),
                requested_at: Instant::now(),
                autorequest: false,
            },
        );

        // BATCH +1 chathistory #test (with draft/chathistory-end tag)
        client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::from([(
                        "draft/chathistory-end".to_string(),
                        String::new(),
                    )]),
                    source: Some(server.clone()),
                    command: Command::BATCH(
                        "+1".to_string(),
                        vec!["chathistory".to_string(), "#test".to_string()],
                    ),
                }),
                None,
                &config,
            )
            .unwrap();

        // Send exactly `limit` messages to test for boundary edge case
        for _ in 0..limit {
            client
                .handle(
                    message::Encoded(proto::Message {
                        tags: BTreeMap::from([(
                            "batch".to_string(),
                            "1".to_string(),
                        )]),
                        source: Some(alice.clone()),
                        command: Command::PRIVMSG(
                            "#test".to_string(),
                            "message".to_string(),
                        ),
                    }),
                    None,
                    &config,
                )
                .unwrap();
        }

        // BATCH -1
        client
            .handle(
                message::Encoded(proto::Message {
                    tags: BTreeMap::default(),
                    source: Some(server.clone()),
                    command: Command::BATCH("-1".to_string(), vec![]),
                }),
                None,
                &config,
            )
            .unwrap();

        assert!(
            client.chathistory_exhausted(&target),
            "draft/chathistory-end tag should mark history as exhausted"
        );
    }

    #[test]
    fn chathistory_direct_messages_record_query_in_querymap() {
        let mut client = test_client("tester");
        let canonical = query("FooBar");

        let message = message::Encoded(proto::Message {
            tags: BTreeMap::default(),
            source: Some(proto::Source::User(proto::User {
                nickname: "FooBar".to_string(),
                username: Some("user".to_string()),
                hostname: Some("example.test".to_string()),
            })),
            command: Command::PRIVMSG(
                "tester".to_string(),
                "hello".to_string(),
            ),
        });

        client.handle_chathistory(message, Target::Query(canonical.clone()));

        assert!(client.querymap.contains_key(&query("foobar")));
        assert_eq!(client.resolve_query(&query("foobar")), Some(&canonical));
    }

    #[test]
    fn ordered_metadata_syncs() {
        let ready_at = Instant::now();
        let foo = Target::parse(
            "#foo",
            isupport::DEFAULT_CHANTYPES,
            isupport::DEFAULT_STATUSMSG,
            isupport::CaseMap::default(),
        );
        let bar = Target::parse(
            "#bar",
            isupport::DEFAULT_CHANTYPES,
            isupport::DEFAULT_STATUSMSG,
            isupport::CaseMap::default(),
        );
        let mut heap = BinaryHeap::from([
            MetadataSync {
                ready_at: ready_at + Duration::from_secs(5),
                target: foo.clone(),
            },
            MetadataSync {
                ready_at,
                target: bar.clone(),
            },
        ]);

        // the earlier item should be popped first
        assert!(heap.pop().is_some_and(|sync| sync.target == bar));

        heap.clear();

        // make sure that we can have two targets at the same time
        heap.push(MetadataSync {
            ready_at,
            target: bar,
        });
        heap.push(MetadataSync {
            ready_at,
            target: foo,
        });

        assert_eq!(heap.len(), 2);
    }
}
