use std::collections::{BTreeMap, HashSet, VecDeque};
use std::sync::{Arc, RwLock};

use crate::thread_bus::ThreadSenders;
use crate::{
    os_input_output::ServerOsApi,
    panes::PaneId,
    plugins::PluginInstruction,
    pty::{ClientTabIndexOrPaneId, PtyInstruction},
    screen::ScreenInstruction,
    ServerInstruction, SessionMetaData, SessionState,
};
use std::thread;
use std::time::Duration;
use uuid::Uuid;
use zellij_utils::{
    channels::SenderWithContext,
    data::{Direction, Event, InputMode, PluginCapabilities, ResizeStrategy},
    errors::prelude::*,
    input::{
        actions::{Action, SearchDirection, SearchOption},
        command::TerminalAction,
        get_mode_info,
        keybinds::Keybinds,
        layout::Layout,
    },
    ipc::{
        ClientAttributes, ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg,
    },
};

use crate::ClientId;

pub(crate) fn route_action(
    action: Action,
    client_id: ClientId,
    pane_id: Option<PaneId>,
    senders: ThreadSenders,
    capabilities: PluginCapabilities,
    client_attributes: ClientAttributes,
    default_shell: Option<TerminalAction>,
    default_layout: Box<Layout>,
    mut seen_cli_pipes: Option<&mut HashSet<String>>,
    client_keybinds: Keybinds,
    default_mode: InputMode,
) -> Result<bool> {
    let mut should_break = false;
    let err_context = || format!("failed to route action for client {client_id}");

    if !action.is_mouse_action() {
        // mouse actions should only send InputReceived to plugins
        // if they do not result in text being marked, this is handled in Tab
        senders
            .send_to_plugin(PluginInstruction::Update(vec![(
                None,
                Some(client_id),
                Event::InputReceived,
            )]))
            .with_context(err_context)?;
    }

    match action {
        Action::ToggleTab => {
            senders
                .send_to_screen(ScreenInstruction::ToggleTab(client_id))
                .with_context(err_context)?;
        },
        Action::Write(key_with_modifier, raw_bytes, is_kitty_keyboard_protocol) => {
            senders
                .send_to_screen(ScreenInstruction::ClearScroll(client_id))
                .with_context(err_context)?;
            senders
                .send_to_screen(ScreenInstruction::WriteCharacter(
                    key_with_modifier,
                    raw_bytes,
                    is_kitty_keyboard_protocol,
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::WriteChars(val) => {
            senders
                .send_to_screen(ScreenInstruction::ClearScroll(client_id))
                .with_context(err_context)?;
            let val = val.into_bytes();
            senders
                .send_to_screen(ScreenInstruction::WriteCharacter(
                    None, val, false, client_id,
                ))
                .with_context(err_context)?;
        },
        Action::SwitchToMode(mode) => {
            let attrs = &client_attributes;
            senders
                .send_to_server(ServerInstruction::ChangeMode(client_id, mode))
                .with_context(err_context)?;
            senders
                .send_to_screen(ScreenInstruction::ChangeMode(
                    get_mode_info(
                        mode,
                        attrs,
                        capabilities,
                        &client_keybinds,
                        Some(default_mode),
                    ),
                    client_id,
                ))
                .with_context(err_context)?;
            senders
                .send_to_screen(ScreenInstruction::Render)
                .with_context(err_context)?;
        },
        Action::Resize(resize, direction) => {
            let screen_instr =
                ScreenInstruction::Resize(client_id, ResizeStrategy::new(resize, direction));
            senders
                .send_to_screen(screen_instr)
                .with_context(err_context)?;
        },
        Action::SwitchFocus => {
            senders
                .send_to_screen(ScreenInstruction::SwitchFocus(client_id))
                .with_context(err_context)?;
        },
        Action::FocusNextPane => {
            senders
                .send_to_screen(ScreenInstruction::FocusNextPane(client_id))
                .with_context(err_context)?;
        },
        Action::FocusPreviousPane => {
            senders
                .send_to_screen(ScreenInstruction::FocusPreviousPane(client_id))
                .with_context(err_context)?;
        },
        Action::MoveFocus(direction) => {
            let screen_instr = match direction {
                Direction::Left => ScreenInstruction::MoveFocusLeft(client_id),
                Direction::Right => ScreenInstruction::MoveFocusRight(client_id),
                Direction::Up => ScreenInstruction::MoveFocusUp(client_id),
                Direction::Down => ScreenInstruction::MoveFocusDown(client_id),
            };
            senders
                .send_to_screen(screen_instr)
                .with_context(err_context)?;
        },
        Action::MoveFocusOrTab(direction) => {
            let screen_instr = match direction {
                Direction::Left => ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id),
                Direction::Right => ScreenInstruction::MoveFocusRightOrNextTab(client_id),
                Direction::Up => ScreenInstruction::SwitchTabNext(client_id),
                Direction::Down => ScreenInstruction::SwitchTabPrev(client_id),
            };
            senders
                .send_to_screen(screen_instr)
                .with_context(err_context)?;
        },
        Action::MovePane(direction) => {
            let screen_instr = match direction {
                Some(Direction::Left) => ScreenInstruction::MovePaneLeft(client_id),
                Some(Direction::Right) => ScreenInstruction::MovePaneRight(client_id),
                Some(Direction::Up) => ScreenInstruction::MovePaneUp(client_id),
                Some(Direction::Down) => ScreenInstruction::MovePaneDown(client_id),
                None => ScreenInstruction::MovePane(client_id),
            };
            senders
                .send_to_screen(screen_instr)
                .with_context(err_context)?;
        },
        Action::MovePaneBackwards => {
            senders
                .send_to_screen(ScreenInstruction::MovePaneBackwards(client_id))
                .with_context(err_context)?;
        },
        Action::ClearScreen => {
            senders
                .send_to_screen(ScreenInstruction::ClearScreen(client_id))
                .with_context(err_context)?;
        },
        Action::DumpScreen(val, full) => {
            senders
                .send_to_screen(ScreenInstruction::DumpScreen(val, client_id, full))
                .with_context(err_context)?;
        },
        Action::DumpLayout => {
            let default_shell = match default_shell {
                Some(TerminalAction::RunCommand(run_command)) => Some(run_command.command),
                _ => None,
            };
            senders
                .send_to_screen(ScreenInstruction::DumpLayout(default_shell, client_id))
                .with_context(err_context)?;
        },
        Action::EditScrollback => {
            senders
                .send_to_screen(ScreenInstruction::EditScrollback(client_id))
                .with_context(err_context)?;
        },
        Action::ScrollUp => {
            senders
                .send_to_screen(ScreenInstruction::ScrollUp(client_id))
                .with_context(err_context)?;
        },
        Action::ScrollUpAt(point) => {
            senders
                .send_to_screen(ScreenInstruction::ScrollUpAt(point, client_id))
                .with_context(err_context)?;
        },
        Action::ScrollDown => {
            senders
                .send_to_screen(ScreenInstruction::ScrollDown(client_id))
                .with_context(err_context)?;
        },
        Action::ScrollDownAt(point) => {
            senders
                .send_to_screen(ScreenInstruction::ScrollDownAt(point, client_id))
                .with_context(err_context)?;
        },
        Action::ScrollToBottom => {
            senders
                .send_to_screen(ScreenInstruction::ScrollToBottom(client_id))
                .with_context(err_context)?;
        },
        Action::ScrollToTop => {
            senders
                .send_to_screen(ScreenInstruction::ScrollToTop(client_id))
                .with_context(err_context)?;
        },
        Action::PageScrollUp => {
            senders
                .send_to_screen(ScreenInstruction::PageScrollUp(client_id))
                .with_context(err_context)?;
        },
        Action::PageScrollDown => {
            senders
                .send_to_screen(ScreenInstruction::PageScrollDown(client_id))
                .with_context(err_context)?;
        },
        Action::HalfPageScrollUp => {
            senders
                .send_to_screen(ScreenInstruction::HalfPageScrollUp(client_id))
                .with_context(err_context)?;
        },
        Action::HalfPageScrollDown => {
            senders
                .send_to_screen(ScreenInstruction::HalfPageScrollDown(client_id))
                .with_context(err_context)?;
        },
        Action::ToggleFocusFullscreen => {
            senders
                .send_to_screen(ScreenInstruction::ToggleActiveTerminalFullscreen(client_id))
                .with_context(err_context)?;
        },
        Action::TogglePaneFrames => {
            senders
                .send_to_screen(ScreenInstruction::TogglePaneFrames)
                .with_context(err_context)?;
        },
        Action::NewPane(direction, name, start_suppressed) => {
            let shell = default_shell.clone();
            let pty_instr = match direction {
                Some(Direction::Left) => {
                    PtyInstruction::SpawnTerminalVertically(shell, name, client_id)
                },
                Some(Direction::Right) => {
                    PtyInstruction::SpawnTerminalVertically(shell, name, client_id)
                },
                Some(Direction::Up) => {
                    PtyInstruction::SpawnTerminalHorizontally(shell, name, client_id)
                },
                Some(Direction::Down) => {
                    PtyInstruction::SpawnTerminalHorizontally(shell, name, client_id)
                },
                // No direction specified - try to put it in the biggest available spot
                None => PtyInstruction::SpawnTerminal(
                    shell,
                    None,
                    name,
                    None,
                    start_suppressed,
                    ClientTabIndexOrPaneId::ClientId(client_id),
                ),
            };
            senders.send_to_pty(pty_instr).with_context(err_context)?;
        },
        Action::EditFile(
            open_file_payload,
            split_direction,
            should_float,
            should_open_in_place,
            start_suppressed,
            floating_pane_coordinates,
        ) => {
            let title = format!("Editing: {}", open_file_payload.path.display());
            let open_file = TerminalAction::OpenFile(open_file_payload);
            let pty_instr = match (split_direction, should_float, should_open_in_place) {
                (Some(Direction::Left), false, false) => {
                    PtyInstruction::SpawnTerminalVertically(Some(open_file), Some(title), client_id)
                },
                (Some(Direction::Right), false, false) => {
                    PtyInstruction::SpawnTerminalVertically(Some(open_file), Some(title), client_id)
                },
                (Some(Direction::Up), false, false) => PtyInstruction::SpawnTerminalHorizontally(
                    Some(open_file),
                    Some(title),
                    client_id,
                ),
                (Some(Direction::Down), false, false) => PtyInstruction::SpawnTerminalHorizontally(
                    Some(open_file),
                    Some(title),
                    client_id,
                ),
                // open terminal in place
                (_, _, true) => match pane_id {
                    Some(pane_id) => PtyInstruction::SpawnInPlaceTerminal(
                        Some(open_file),
                        Some(title),
                        false,
                        ClientTabIndexOrPaneId::PaneId(pane_id),
                    ),
                    None => PtyInstruction::SpawnInPlaceTerminal(
                        Some(open_file),
                        Some(title),
                        false,
                        ClientTabIndexOrPaneId::ClientId(client_id),
                    ),
                },
                // Open either floating terminal if we were asked with should_float or defer
                // placement to screen
                (None, _, _) | (_, true, _) => PtyInstruction::SpawnTerminal(
                    Some(open_file),
                    Some(should_float),
                    Some(title),
                    floating_pane_coordinates,
                    start_suppressed,
                    ClientTabIndexOrPaneId::ClientId(client_id),
                ),
            };
            senders.send_to_pty(pty_instr).with_context(err_context)?;
        },
        Action::SwitchModeForAllClients(input_mode) => {
            let attrs = &client_attributes;
            senders
                .send_to_plugin(PluginInstruction::Update(vec![(
                    None,
                    None,
                    Event::ModeUpdate(get_mode_info(
                        input_mode,
                        attrs,
                        capabilities,
                        &client_keybinds,
                        Some(default_mode),
                    )),
                )]))
                .with_context(err_context)?;

            senders
                .send_to_server(ServerInstruction::ChangeModeForAllClients(input_mode))
                .with_context(err_context)?;

            senders
                .send_to_screen(ScreenInstruction::ChangeModeForAllClients(get_mode_info(
                    input_mode,
                    attrs,
                    capabilities,
                    &client_keybinds,
                    Some(default_mode),
                )))
                .with_context(err_context)?;
        },
        Action::NewFloatingPane(run_command, name, floating_pane_coordinates) => {
            let should_float = true;
            let run_cmd = run_command
                .map(|cmd| TerminalAction::RunCommand(cmd.into()))
                .or_else(|| default_shell.clone());
            senders
                .send_to_pty(PtyInstruction::SpawnTerminal(
                    run_cmd,
                    Some(should_float),
                    name,
                    floating_pane_coordinates,
                    false,
                    ClientTabIndexOrPaneId::ClientId(client_id),
                ))
                .with_context(err_context)?;
        },
        Action::NewInPlacePane(run_command, name) => {
            let run_cmd = run_command
                .map(|cmd| TerminalAction::RunCommand(cmd.into()))
                .or_else(|| default_shell.clone());
            match pane_id {
                Some(pane_id) => {
                    senders
                        .send_to_pty(PtyInstruction::SpawnInPlaceTerminal(
                            run_cmd,
                            name,
                            false,
                            ClientTabIndexOrPaneId::PaneId(pane_id),
                        ))
                        .with_context(err_context)?;
                },
                None => {
                    senders
                        .send_to_pty(PtyInstruction::SpawnInPlaceTerminal(
                            run_cmd,
                            name,
                            false,
                            ClientTabIndexOrPaneId::ClientId(client_id),
                        ))
                        .with_context(err_context)?;
                },
            }
        },
        Action::NewTiledPane(direction, run_command, name) => {
            let should_float = false;
            let run_cmd = run_command
                .map(|cmd| TerminalAction::RunCommand(cmd.into()))
                .or_else(|| default_shell.clone());
            let pty_instr = match direction {
                Some(Direction::Left) => {
                    PtyInstruction::SpawnTerminalVertically(run_cmd, name, client_id)
                },
                Some(Direction::Right) => {
                    PtyInstruction::SpawnTerminalVertically(run_cmd, name, client_id)
                },
                Some(Direction::Up) => {
                    PtyInstruction::SpawnTerminalHorizontally(run_cmd, name, client_id)
                },
                Some(Direction::Down) => {
                    PtyInstruction::SpawnTerminalHorizontally(run_cmd, name, client_id)
                },
                // No direction specified - try to put it in the biggest available spot
                None => PtyInstruction::SpawnTerminal(
                    run_cmd,
                    Some(should_float),
                    name,
                    None,
                    false,
                    ClientTabIndexOrPaneId::ClientId(client_id),
                ),
            };
            senders.send_to_pty(pty_instr).with_context(err_context)?;
        },
        Action::TogglePaneEmbedOrFloating => {
            senders
                .send_to_screen(ScreenInstruction::TogglePaneEmbedOrFloating(client_id))
                .with_context(err_context)?;
        },
        Action::ToggleFloatingPanes => {
            senders
                .send_to_screen(ScreenInstruction::ToggleFloatingPanes(
                    client_id,
                    default_shell.clone(),
                ))
                .with_context(err_context)?;
        },
        Action::PaneNameInput(c) => {
            senders
                .send_to_screen(ScreenInstruction::UpdatePaneName(c, client_id))
                .with_context(err_context)?;
        },
        Action::UndoRenamePane => {
            senders
                .send_to_screen(ScreenInstruction::UndoRenamePane(client_id))
                .with_context(err_context)?;
        },
        Action::Run(command) => {
            let run_cmd = Some(TerminalAction::RunCommand(command.clone().into()));
            let pty_instr = match command.direction {
                Some(Direction::Left) => {
                    PtyInstruction::SpawnTerminalVertically(run_cmd, None, client_id)
                },
                Some(Direction::Right) => {
                    PtyInstruction::SpawnTerminalVertically(run_cmd, None, client_id)
                },
                Some(Direction::Up) => {
                    PtyInstruction::SpawnTerminalHorizontally(run_cmd, None, client_id)
                },
                Some(Direction::Down) => {
                    PtyInstruction::SpawnTerminalHorizontally(run_cmd, None, client_id)
                },
                // No direction specified - try to put it in the biggest available spot
                None => PtyInstruction::SpawnTerminal(
                    run_cmd,
                    None,
                    None,
                    None,
                    false,
                    ClientTabIndexOrPaneId::ClientId(client_id),
                ),
            };
            senders.send_to_pty(pty_instr).with_context(err_context)?;
        },
        Action::CloseFocus => {
            senders
                .send_to_screen(ScreenInstruction::CloseFocusedPane(client_id))
                .with_context(err_context)?;
        },
        Action::NewTab(
            tab_layout,
            floating_panes_layout,
            swap_tiled_layouts,
            swap_floating_layouts,
            tab_name,
            should_change_focus_to_new_tab,
        ) => {
            let shell = default_shell.clone();
            let swap_tiled_layouts =
                swap_tiled_layouts.unwrap_or_else(|| default_layout.swap_tiled_layouts.clone());
            let swap_floating_layouts = swap_floating_layouts
                .unwrap_or_else(|| default_layout.swap_floating_layouts.clone());
            let is_web_client = false; // actions cannot be initiated directly from the web
            senders
                .send_to_screen(ScreenInstruction::NewTab(
                    None,
                    shell,
                    tab_layout,
                    floating_panes_layout,
                    tab_name,
                    (swap_tiled_layouts, swap_floating_layouts),
                    should_change_focus_to_new_tab,
                    (client_id, is_web_client),
                ))
                .with_context(err_context)?;
        },
        Action::GoToNextTab => {
            senders
                .send_to_screen(ScreenInstruction::SwitchTabNext(client_id))
                .with_context(err_context)?;
        },
        Action::GoToPreviousTab => {
            senders
                .send_to_screen(ScreenInstruction::SwitchTabPrev(client_id))
                .with_context(err_context)?;
        },
        Action::ToggleActiveSyncTab => {
            senders
                .send_to_screen(ScreenInstruction::ToggleActiveSyncTab(client_id))
                .with_context(err_context)?;
        },
        Action::CloseTab => {
            senders
                .send_to_screen(ScreenInstruction::CloseTab(client_id))
                .with_context(err_context)?;
        },
        Action::GoToTab(i) => {
            senders
                .send_to_screen(ScreenInstruction::GoToTab(i, Some(client_id)))
                .with_context(err_context)?;
        },
        Action::GoToTabName(name, create) => {
            let shell = default_shell.clone();
            let swap_tiled_layouts = default_layout.swap_tiled_layouts.clone();
            let swap_floating_layouts = default_layout.swap_floating_layouts.clone();
            senders
                .send_to_screen(ScreenInstruction::GoToTabName(
                    name,
                    (swap_tiled_layouts, swap_floating_layouts),
                    shell,
                    create,
                    Some(client_id),
                ))
                .with_context(err_context)?;
        },
        Action::TabNameInput(c) => {
            senders
                .send_to_screen(ScreenInstruction::UpdateTabName(c, client_id))
                .with_context(err_context)?;
        },
        Action::UndoRenameTab => {
            senders
                .send_to_screen(ScreenInstruction::UndoRenameTab(client_id))
                .with_context(err_context)?;
        },
        Action::MoveTab(direction) => {
            let screen_instr = match direction {
                Direction::Left => ScreenInstruction::MoveTabLeft(client_id),
                Direction::Right => ScreenInstruction::MoveTabRight(client_id),
                _ => return Ok(false),
            };
            senders
                .send_to_screen(screen_instr)
                .with_context(err_context)?;
        },
        Action::Quit => {
            senders
                .send_to_server(ServerInstruction::ClientExit(client_id))
                .with_context(err_context)?;
            should_break = true;
        },
        Action::Detach => {
            senders
                .send_to_server(ServerInstruction::DetachSession(vec![client_id]))
                .with_context(err_context)?;
            should_break = true;
        },
        Action::MouseEvent(event) => {
            senders
                .send_to_screen(ScreenInstruction::MouseEvent(event, client_id))
                .with_context(err_context)?;
        },
        Action::Copy => {
            senders
                .send_to_screen(ScreenInstruction::Copy(client_id))
                .with_context(err_context)?;
        },
        Action::Confirm => {
            senders
                .send_to_screen(ScreenInstruction::ConfirmPrompt(client_id))
                .with_context(err_context)?;
        },
        Action::Deny => {
            senders
                .send_to_screen(ScreenInstruction::DenyPrompt(client_id))
                .with_context(err_context)?;
        },
        #[allow(clippy::single_match)]
        Action::SkipConfirm(action) => match *action {
            Action::Quit => {
                senders
                    .send_to_server(ServerInstruction::ClientExit(client_id))
                    .with_context(err_context)?;
                should_break = true;
            },
            _ => {},
        },
        Action::NoOp => {},
        Action::SearchInput(c) => {
            senders
                .send_to_screen(ScreenInstruction::UpdateSearch(c, client_id))
                .with_context(err_context)?;
        },
        Action::Search(d) => {
            let instruction = match d {
                SearchDirection::Down => ScreenInstruction::SearchDown(client_id),
                SearchDirection::Up => ScreenInstruction::SearchUp(client_id),
            };
            senders
                .send_to_screen(instruction)
                .with_context(err_context)?;
        },
        Action::SearchToggleOption(o) => {
            let instruction = match o {
                SearchOption::CaseSensitivity => {
                    ScreenInstruction::SearchToggleCaseSensitivity(client_id)
                },
                SearchOption::WholeWord => ScreenInstruction::SearchToggleWholeWord(client_id),
                SearchOption::Wrap => ScreenInstruction::SearchToggleWrap(client_id),
            };
            senders
                .send_to_screen(instruction)
                .with_context(err_context)?;
        },
        Action::ToggleMouseMode => {}, // Handled client side
        Action::PreviousSwapLayout => {
            senders
                .send_to_screen(ScreenInstruction::PreviousSwapLayout(client_id))
                .with_context(err_context)?;
        },
        Action::NextSwapLayout => {
            senders
                .send_to_screen(ScreenInstruction::NextSwapLayout(client_id))
                .with_context(err_context)?;
        },
        Action::QueryTabNames => {
            senders
                .send_to_screen(ScreenInstruction::QueryTabNames(client_id))
                .with_context(err_context)?;
        },
        Action::NewTiledPluginPane(run_plugin, name, skip_cache, cwd) => {
            senders
                .send_to_screen(ScreenInstruction::NewTiledPluginPane(
                    run_plugin, name, skip_cache, cwd, client_id,
                ))
                .with_context(err_context)?;
        },
        Action::NewFloatingPluginPane(
            run_plugin,
            name,
            skip_cache,
            cwd,
            floating_pane_coordinates,
        ) => {
            senders
                .send_to_screen(ScreenInstruction::NewFloatingPluginPane(
                    run_plugin,
                    name,
                    skip_cache,
                    cwd,
                    floating_pane_coordinates,
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::NewInPlacePluginPane(run_plugin, name, skip_cache) => {
            if let Some(pane_id) = pane_id {
                senders
                    .send_to_screen(ScreenInstruction::NewInPlacePluginPane(
                        run_plugin, name, pane_id, skip_cache, client_id,
                    ))
                    .with_context(err_context)?;
            } else {
                log::error!("Must have pane_id in order to open in place pane");
            }
        },
        Action::StartOrReloadPlugin(run_plugin) => {
            senders
                .send_to_screen(ScreenInstruction::StartOrReloadPluginPane(run_plugin, None))
                .with_context(err_context)?;
        },
        Action::LaunchOrFocusPlugin(
            run_plugin,
            should_float,
            move_to_focused_tab,
            should_open_in_place,
            skip_cache,
        ) => {
            senders
                .send_to_screen(ScreenInstruction::LaunchOrFocusPlugin(
                    run_plugin,
                    should_float,
                    move_to_focused_tab,
                    should_open_in_place,
                    pane_id,
                    skip_cache,
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::LaunchPlugin(run_plugin, should_float, should_open_in_place, skip_cache, cwd) => {
            senders
                .send_to_screen(ScreenInstruction::LaunchPlugin(
                    run_plugin,
                    should_float,
                    should_open_in_place,
                    pane_id,
                    skip_cache,
                    cwd,
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::CloseTerminalPane(terminal_pane_id) => {
            senders
                .send_to_screen(ScreenInstruction::ClosePane(
                    PaneId::Terminal(terminal_pane_id),
                    None, // we send None here so that the terminal pane would be closed anywhere
                          // in the app, not just in the client's tab
                ))
                .with_context(err_context)?;
        },
        Action::ClosePluginPane(plugin_pane_id) => {
            senders
                .send_to_screen(ScreenInstruction::ClosePane(
                    PaneId::Plugin(plugin_pane_id),
                    None, // we send None here so that the terminal pane would be closed anywhere
                          // in the app, not just in the client's tab
                ))
                .with_context(err_context)?;
        },
        Action::FocusTerminalPaneWithId(pane_id, should_float_if_hidden) => {
            senders
                .send_to_screen(ScreenInstruction::FocusPaneWithId(
                    PaneId::Terminal(pane_id),
                    should_float_if_hidden,
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::FocusPluginPaneWithId(pane_id, should_float_if_hidden) => {
            senders
                .send_to_screen(ScreenInstruction::FocusPaneWithId(
                    PaneId::Plugin(pane_id),
                    should_float_if_hidden,
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::RenameTerminalPane(pane_id, name_bytes) => {
            senders
                .send_to_screen(ScreenInstruction::RenamePane(
                    PaneId::Terminal(pane_id),
                    name_bytes,
                ))
                .with_context(err_context)?;
        },
        Action::RenamePluginPane(pane_id, name_bytes) => {
            senders
                .send_to_screen(ScreenInstruction::RenamePane(
                    PaneId::Plugin(pane_id),
                    name_bytes,
                ))
                .with_context(err_context)?;
        },
        Action::RenameTab(tab_position, name_bytes) => {
            senders
                .send_to_screen(ScreenInstruction::RenameTab(
                    tab_position as usize,
                    name_bytes,
                ))
                .with_context(err_context)?;
        },
        Action::BreakPane => {
            senders
                .send_to_screen(ScreenInstruction::BreakPane(
                    default_layout.clone(),
                    default_shell.clone(),
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::BreakPaneRight => {
            senders
                .send_to_screen(ScreenInstruction::BreakPaneRight(client_id))
                .with_context(err_context)?;
        },
        Action::BreakPaneLeft => {
            senders
                .send_to_screen(ScreenInstruction::BreakPaneLeft(client_id))
                .with_context(err_context)?;
        },
        Action::RenameSession(name) => {
            senders
                .send_to_screen(ScreenInstruction::RenameSession(name, client_id))
                .with_context(err_context)?;
        },
        Action::CliPipe {
            pipe_id,
            mut name,
            payload,
            plugin,
            args,
            configuration,
            floating,
            in_place,
            skip_cache,
            cwd,
            pane_title,
            ..
        } => {
            if let Some(seen_cli_pipes) = seen_cli_pipes.as_mut() {
                if !seen_cli_pipes.contains(&pipe_id) {
                    seen_cli_pipes.insert(pipe_id.clone());
                    senders
                        .send_to_server(ServerInstruction::AssociatePipeWithClient {
                            pipe_id: pipe_id.clone(),
                            client_id,
                        })
                        .with_context(err_context)?;
                }
            }
            if let Some(name) = name.take() {
                let should_open_in_place = in_place.unwrap_or(false);
                if should_open_in_place && pane_id.is_none() {
                    log::error!("Was asked to open a new plugin in-place, but cannot identify the pane id... is the ZELLIJ_PANE_ID variable set?");
                }
                let pane_id_to_replace = if should_open_in_place { pane_id } else { None };
                senders
                    .send_to_plugin(PluginInstruction::CliPipe {
                        pipe_id,
                        name,
                        payload,
                        plugin,
                        args,
                        configuration,
                        floating,
                        pane_id_to_replace,
                        cwd,
                        pane_title,
                        skip_cache,
                        cli_client_id: client_id,
                    })
                    .with_context(err_context)?;
            } else {
                log::error!("Message must have a name");
            }
        },
        Action::KeybindPipe {
            mut name,
            payload,
            plugin,
            args,
            mut configuration,
            floating,
            in_place,
            skip_cache,
            cwd,
            pane_title,
            launch_new,
            plugin_id,
            ..
        } => {
            if let Some(name) = name.take() {
                let should_open_in_place = in_place.unwrap_or(false);
                let pane_id_to_replace = if should_open_in_place { pane_id } else { None };
                if launch_new && plugin_id.is_none() {
                    // we do this to make sure the plugin is unique (has a unique configuration parameter)
                    configuration
                        .get_or_insert_with(BTreeMap::new)
                        .insert("_zellij_id".to_owned(), Uuid::new_v4().to_string());
                }
                senders
                    .send_to_plugin(PluginInstruction::KeybindPipe {
                        name,
                        payload,
                        plugin,
                        args,
                        configuration,
                        floating,
                        pane_id_to_replace,
                        cwd,
                        pane_title,
                        skip_cache,
                        cli_client_id: client_id,
                        plugin_and_client_id: plugin_id.map(|plugin_id| (plugin_id, client_id)),
                    })
                    .with_context(err_context)?;
            } else {
                log::error!("Message must have a name");
            }
        },
        Action::ListClients => {
            let default_shell = match default_shell {
                Some(TerminalAction::RunCommand(run_command)) => Some(run_command.command),
                _ => None,
            };
            senders
                .send_to_screen(ScreenInstruction::ListClientsMetadata(
                    default_shell,
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::TogglePanePinned => {
            senders
                .send_to_screen(ScreenInstruction::TogglePanePinned(client_id))
                .with_context(err_context)?;
        },
        Action::StackPanes(pane_ids_to_stack) => {
            senders
                .send_to_screen(ScreenInstruction::StackPanes(
                    pane_ids_to_stack.iter().map(|p| PaneId::from(*p)).collect(),
                    client_id,
                ))
                .with_context(err_context)?;
        },
        Action::ChangeFloatingPaneCoordinates(pane_id, coordinates) => {
            senders
                .send_to_screen(ScreenInstruction::ChangeFloatingPanesCoordinates(vec![(
                    pane_id.into(),
                    coordinates,
                )]))
                .with_context(err_context)?;
        },
        Action::TogglePaneInGroup => {
            senders
                .send_to_screen(ScreenInstruction::TogglePaneInGroup(client_id))
                .with_context(err_context)?;
        },
        Action::ToggleGroupMarking => {
            senders
                .send_to_screen(ScreenInstruction::ToggleGroupMarking(client_id))
                .with_context(err_context)?;
        },
    }
    Ok(should_break)
}

// this should only be used for one-off startup instructions
macro_rules! send_to_screen_or_retry_queue {
    ($rlocked_sessions:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{
        match $rlocked_sessions.as_ref() {
            Some(session_metadata) => session_metadata.senders.send_to_screen($message),
            None => {
                log::warn!("Server not ready, trying to place instruction in retry queue...");
                if let Some(retry_queue) = $retry_queue.as_mut() {
                    retry_queue.push_back($instruction);
                }
                Ok(())
            },
        }
    }};
}

pub(crate) fn route_thread_main(
    session_data: Arc<RwLock<Option<SessionMetaData>>>,
    session_state: Arc<RwLock<SessionState>>,
    os_input: Box<dyn ServerOsApi>,
    to_server: SenderWithContext<ServerInstruction>,
    mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
    client_id: ClientId,
) -> Result<()> {
    let mut retry_queue = VecDeque::new();
    let err_context = || format!("failed to handle instruction for client {client_id}");
    let mut seen_cli_pipes = HashSet::new();
    'route_loop: loop {
        match receiver.recv() {
            Some((instruction, err_ctx)) => {
                err_ctx.update_thread_ctx();
                let mut handle_instruction = |instruction: ClientToServerMsg,
                                              mut retry_queue: Option<
                    &mut VecDeque<ClientToServerMsg>,
                >|
                 -> Result<bool> {
                    let mut should_break = false;
                    let rlocked_sessions =
                        session_data.read().to_anyhow().with_context(err_context)?;
                    match instruction {
                        ClientToServerMsg::Key(key, raw_bytes, is_kitty_keyboard_protocol) => {
                            if let Some(rlocked_sessions) = rlocked_sessions.as_ref() {
                                match rlocked_sessions.get_client_keybinds_and_mode(&client_id) {
                                    Some((keybinds, input_mode, default_input_mode)) => {
                                        for action in keybinds
                                            .get_actions_for_key_in_mode_or_default_action(
                                                &input_mode,
                                                &key,
                                                raw_bytes,
                                                default_input_mode,
                                                is_kitty_keyboard_protocol,
                                            )
                                        {
                                            if route_action(
                                                action,
                                                client_id,
                                                None,
                                                rlocked_sessions.senders.clone(),
                                                rlocked_sessions.capabilities.clone(),
                                                rlocked_sessions.client_attributes.clone(),
                                                rlocked_sessions.default_shell.clone(),
                                                rlocked_sessions.layout.clone(),
                                                Some(&mut seen_cli_pipes),
                                                keybinds.clone(),
                                                rlocked_sessions
                                                    .session_configuration
                                                    .get_client_configuration(&client_id)
                                                    .options
                                                    .default_mode
                                                    .unwrap_or(InputMode::Normal)
                                                    .clone(),
                                            )? {
                                                should_break = true;
                                            }
                                        }
                                    },
                                    None => {
                                        log::error!("Failed to get keybindings for client");
                                    },
                                }
                            }
                        },
                        ClientToServerMsg::Action(action, maybe_pane_id, maybe_client_id) => {
                            let client_id = maybe_client_id.unwrap_or(client_id);
                            if let Some(rlocked_sessions) = rlocked_sessions.as_ref() {
                                if route_action(
                                    action,
                                    client_id,
                                    maybe_pane_id.map(|p| PaneId::Terminal(p)),
                                    rlocked_sessions.senders.clone(),
                                    rlocked_sessions.capabilities.clone(),
                                    rlocked_sessions.client_attributes.clone(),
                                    rlocked_sessions.default_shell.clone(),
                                    rlocked_sessions.layout.clone(),
                                    Some(&mut seen_cli_pipes),
                                    rlocked_sessions
                                        .session_configuration
                                        .get_client_keybinds(&client_id)
                                        .clone(),
                                    rlocked_sessions
                                        .session_configuration
                                        .get_client_configuration(&client_id)
                                        .options
                                        .default_mode
                                        .unwrap_or(InputMode::Normal)
                                        .clone(),
                                )? {
                                    should_break = true;
                                }
                            }
                        },
                        ClientToServerMsg::TerminalResize(new_size) => {
                            session_state
                                .write()
                                .to_anyhow()
                                .with_context(err_context)?
                                .set_client_size(client_id, new_size);
                            session_state
                                .read()
                                .to_anyhow()
                                .and_then(|state| {
                                    state.min_client_terminal_size().ok_or(anyhow!(
                                        "failed to determine minimal client terminal size"
                                    ))
                                })
                                .and_then(|min_size| {
                                    rlocked_sessions
                                        .as_ref()
                                        .context("couldn't get reference to read-locked session")?
                                        .senders
                                        .send_to_screen(ScreenInstruction::TerminalResize(min_size))
                                })
                                .with_context(err_context)?;
                        },
                        ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions) => {
                            send_to_screen_or_retry_queue!(
                                rlocked_sessions,
                                ScreenInstruction::TerminalPixelDimensions(pixel_dimensions),
                                instruction,
                                retry_queue
                            )
                            .with_context(err_context)?;
                        },
                        ClientToServerMsg::BackgroundColor(ref background_color_instruction) => {
                            send_to_screen_or_retry_queue!(
                                rlocked_sessions,
                                ScreenInstruction::TerminalBackgroundColor(
                                    background_color_instruction.clone()
                                ),
                                instruction,
                                retry_queue
                            )
                            .with_context(err_context)?;
                        },
                        ClientToServerMsg::ForegroundColor(ref foreground_color_instruction) => {
                            send_to_screen_or_retry_queue!(
                                rlocked_sessions,
                                ScreenInstruction::TerminalForegroundColor(
                                    foreground_color_instruction.clone()
                                ),
                                instruction,
                                retry_queue
                            )
                            .with_context(err_context)?;
                        },
                        ClientToServerMsg::ColorRegisters(ref color_registers) => {
                            send_to_screen_or_retry_queue!(
                                rlocked_sessions,
                                ScreenInstruction::TerminalColorRegisters(color_registers.clone()),
                                instruction,
                                retry_queue
                            )
                            .with_context(err_context)?;
                        },
                        ClientToServerMsg::NewClient(
                            client_attributes,
                            cli_args,
                            config,
                            runtime_config_options,
                            layout,
                            plugin_aliases,
                            should_launch_setup_wizard,
                            is_web_client,
                        ) => {
                            let new_client_instruction = ServerInstruction::NewClient(
                                client_attributes,
                                cli_args,
                                config,
                                runtime_config_options,
                                layout,
                                plugin_aliases,
                                should_launch_setup_wizard,
                                is_web_client,
                                client_id,
                            );
                            to_server
                                .send(new_client_instruction)
                                .with_context(err_context)?;
                        },
                        ClientToServerMsg::AttachClient(
                            client_attributes,
                            config,
                            runtime_config_options,
                            tab_position_to_focus,
                            pane_id_to_focus,
                            is_web_client,
                        ) => {
                            let allow_web_connections = rlocked_sessions
                                .as_ref()
                                .map(|rlocked_sessions| {
                                    rlocked_sessions.web_sharing.web_clients_allowed()
                                })
                                .unwrap_or(false);
                            let should_allow_connection = !is_web_client || allow_web_connections;
                            if should_allow_connection {
                                let attach_client_instruction = ServerInstruction::AttachClient(
                                    client_attributes,
                                    config,
                                    runtime_config_options,
                                    tab_position_to_focus,
                                    pane_id_to_focus,
                                    is_web_client,
                                    client_id,
                                );
                                to_server
                                    .send(attach_client_instruction)
                                    .with_context(err_context)?;
                            } else {
                                let error = "This session does not allow web connections.";
                                let _ = to_server.send(ServerInstruction::LogError(
                                    vec![error.to_owned()],
                                    client_id,
                                ));
                                let _ = to_server
                                    .send(ServerInstruction::SendWebClientsForbidden(client_id));
                            }
                        },
                        ClientToServerMsg::ClientExited => {
                            // we don't unwrap this because we don't really care if there's an error here (eg.
                            // if the main server thread exited before this router thread did)
                            let _ = to_server.send(ServerInstruction::RemoveClient(client_id));
                            return Ok(true);
                        },
                        ClientToServerMsg::KillSession => {
                            to_server
                                .send(ServerInstruction::KillSession)
                                .with_context(err_context)?;
                        },
                        ClientToServerMsg::ConnStatus => {
                            let _ = to_server.send(ServerInstruction::ConnStatus(client_id));
                            should_break = true;
                        },
                        ClientToServerMsg::DetachSession(client_id) => {
                            let _ = to_server.send(ServerInstruction::DetachSession(client_id));
                            should_break = true;
                        },
                        ClientToServerMsg::ConfigWrittenToDisk(config) => {
                            let _ = to_server
                                .send(ServerInstruction::ConfigWrittenToDisk(client_id, config));
                        },
                        ClientToServerMsg::FailedToWriteConfigToDisk(failed_path) => {
                            let _ = to_server.send(ServerInstruction::FailedToWriteConfigToDisk(
                                client_id,
                                failed_path,
                            ));
                        },
                        ClientToServerMsg::WebServerStarted(base_url) => {
                            let _ = to_server.send(ServerInstruction::WebServerStarted(base_url));
                        },
                        ClientToServerMsg::FailedToStartWebServer(error) => {
                            let _ =
                                to_server.send(ServerInstruction::FailedToStartWebServer(error));
                        },
                    }
                    Ok(should_break)
                };
                let mut repeat_retries = VecDeque::new();
                while let Some(instruction_to_retry) = retry_queue.pop_front() {
                    log::warn!("Server ready, retrying sending instruction.");
                    thread::sleep(Duration::from_millis(5));
                    let should_break =
                        handle_instruction(instruction_to_retry, Some(&mut repeat_retries))?;
                    if should_break {
                        break 'route_loop;
                    }
                }
                // retry on loop around
                retry_queue.append(&mut repeat_retries);
                let should_break = handle_instruction(instruction, Some(&mut retry_queue))?;
                if should_break {
                    break 'route_loop;
                }
            },
            None => {
                log::error!("Received empty message from client, logging client out.");
                let _ = os_input.send_to_client(
                    client_id,
                    ServerToClientMsg::Exit(ExitReason::Error(
                        "Received empty message".to_string(),
                    )),
                );
                let _ = to_server.send(ServerInstruction::RemoveClient(client_id));
                break 'route_loop;
            },
        }
    }
    Ok(())
}
