// SPDX-FileCopyrightText: 2025 The superseedr Contributors
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::app::{
    App, AppCommand, AppMode, BrowserPane, ConfigItem, ConfigUiState, FileBrowserMode,
    FileMetadata, FilePriority, TorrentDisplayState, TorrentPreviewPayload,
};
use crate::theme::ThemeContext;
use crate::torrent_manager::ManagerCommand;
use crate::tui::formatters::{centered_rect, format_bytes, truncate_with_ellipsis};
use crate::tui::layout::browser::calculate_file_browser_layout;
use crate::tui::screen_context::ScreenContext;
use crate::tui::tree::{RawNode, TreeAction, TreeFilter, TreeMathHelper, TreeViewState};
use ratatui::crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEventKind};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::{Alignment, Frame, Line, Modifier, Span, Style, Stylize};
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph};
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use tokio::sync::mpsc::{self, Sender};

const ASCII_TREE_DIR_ICON: &str = "> ";
const ASCII_TREE_FILE_ICON: &str = "  ";
const ASCII_TREE_ROOT_ICON: &str = "> ";

pub struct DownloadConfirmPayload {
    pub base_path: PathBuf,
    pub container_name_to_use: Option<String>,
    pub file_priorities: HashMap<usize, FilePriority>,
}

pub fn draw(
    f: &mut Frame,
    screen: &ScreenContext<'_>,
    state: &TreeViewState,
    data: &[RawNode<FileMetadata>],
    browser_mode: &FileBrowserMode,
) {
    let app_state = screen.ui;
    let ctx = screen.theme;

    let has_preview_content = has_preview_content(
        browser_mode,
        app_state.pending_torrent_path.is_some(),
        !app_state.pending_torrent_link.is_empty(),
        state.cursor_path.as_ref(),
    );

    let preview_file_path = match browser_mode {
        FileBrowserMode::DownloadLocSelection { .. } => app_state.pending_torrent_path.as_ref(),
        FileBrowserMode::File(_) => state.cursor_path.as_ref(),
        _ => None,
    };

    let focused_pane = focused_pane(browser_mode);
    let max_area = centered_rect(90, 80, f.area());
    f.render_widget(Clear, max_area);

    let area = calculate_area(f.area(), has_preview_content);
    let layout = calculate_file_browser_layout(
        area,
        has_preview_content,
        app_state.ui.file_browser.is_searching,
        &focused_pane,
    );

    let (files_border_style, preview_border_style) =
        if let FileBrowserMode::DownloadLocSelection { focused_pane, .. } = browser_mode {
            match focused_pane {
                BrowserPane::FileSystem => (
                    ctx.apply(Style::default().fg(ctx.state_selected())),
                    ctx.apply(Style::default().fg(ctx.theme.semantic.surface2)),
                ),
                BrowserPane::TorrentPreview => (
                    ctx.apply(Style::default().fg(ctx.theme.semantic.surface2)),
                    ctx.apply(Style::default().fg(ctx.state_selected())),
                ),
            }
        } else {
            (
                ctx.apply(Style::default().fg(ctx.state_selected())),
                ctx.apply(Style::default().fg(ctx.accent_sapphire())),
            )
        };

    if let Some(preview_area) = layout.preview {
        draw_torrent_preview_panel(
            f,
            ctx,
            preview_area,
            preview_file_path.map(|p| p.as_path()),
            browser_mode,
            preview_border_style,
            &state.current_path,
        );
    }
    if let Some(search_area) = layout.search {
        let search_block = Block::default()
            .borders(Borders::ALL)
            .border_style(ctx.apply(Style::default().fg(ctx.state_warning())))
            .title(" Search Filter ");
        let search_text = Line::from(vec![
            Span::styled(
                "/",
                ctx.apply(Style::default().fg(ctx.theme.semantic.subtext0)),
            ),
            Span::raw(&app_state.ui.file_browser.search_query),
            Span::styled(
                "_",
                ctx.apply(
                    Style::default()
                        .fg(ctx.state_warning())
                        .add_modifier(Modifier::SLOW_BLINK),
                ),
            ),
        ]);
        f.render_widget(Paragraph::new(search_text).block(search_block), search_area);
    }

    let mut footer_spans = Vec::new();
    match browser_mode {
        FileBrowserMode::ConfigPathSelection { .. } | FileBrowserMode::Directory => {
            footer_spans.push(Span::styled(
                "[Arrows/Vim]",
                ctx.apply(Style::default().fg(ctx.state_info())),
            ));
            footer_spans.push(Span::raw(" Nav | "));
            footer_spans.push(Span::styled(
                "[Backspace]",
                ctx.apply(Style::default().fg(ctx.state_warning())),
            ));
            footer_spans.push(Span::raw(" Up | "));
            footer_spans.push(Span::styled(
                "[Enter]",
                ctx.apply(Style::default().fg(ctx.state_warning())),
            ));
            footer_spans.push(Span::raw(" Down | "));
            footer_spans.push(Span::styled(
                "[Y]",
                ctx.apply(Style::default().fg(ctx.state_success())),
            ));
            footer_spans.push(Span::raw(" Confirm Selection | "));
        }
        FileBrowserMode::DownloadLocSelection {
            focused_pane,
            use_container,
            ..
        } => {
            footer_spans.push(Span::styled(
                "[Tab]",
                ctx.apply(Style::default().fg(ctx.accent_sapphire())),
            ));
            footer_spans.push(Span::raw(" Switch Pane | "));

            if matches!(focused_pane, BrowserPane::TorrentPreview) {
                footer_spans.push(Span::styled(
                    "[Space]",
                    ctx.apply(Style::default().fg(ctx.state_warning())),
                ));
                footer_spans.push(Span::raw(" Priority | "));
            }

            footer_spans.push(Span::styled(
                "[x]",
                ctx.apply(Style::default().fg(ctx.state_selected())),
            ));
            footer_spans.push(Span::raw(" Container Folder | "));

            if *use_container {
                footer_spans.push(Span::styled(
                    "[r]",
                    ctx.apply(Style::default().fg(ctx.accent_sky())),
                ));
                footer_spans.push(Span::raw(" Rename | "));
            }

            footer_spans.push(Span::styled(
                "[Y]",
                ctx.apply(Style::default().fg(ctx.state_success())),
            ));
            footer_spans.push(Span::raw(" Confirm"));
        }
        FileBrowserMode::File(_) => {
            footer_spans.push(Span::styled(
                "[Y]",
                ctx.apply(Style::default().fg(ctx.state_success())),
            ));
            footer_spans.push(Span::raw(" Confirm File | "));
        }
    }
    footer_spans.push(Span::raw(" | "));
    footer_spans.push(Span::styled(
        "[Esc]",
        ctx.apply(Style::default().fg(ctx.state_error())),
    ));
    footer_spans.push(Span::raw(" Cancel"));

    let footer = Paragraph::new(Line::from(footer_spans))
        .alignment(Alignment::Center)
        .style(ctx.apply(Style::default().fg(ctx.theme.semantic.subtext1)));
    f.render_widget(footer, layout.footer);

    let inner_height = layout.list.height.saturating_sub(2) as usize;
    let list_width = layout.list.width.saturating_sub(2) as usize;
    let filter = build_filter(browser_mode, &app_state.ui.file_browser.search_query);

    let abs_path = state.current_path.to_string_lossy();
    let item_count = data.len();
    let count_label = if item_count == 0 {
        " (empty)".to_string()
    } else {
        format!(" ({} items)", item_count)
    };
    let left_title = format!(" {}/{} ", abs_path, count_label);
    let right_title = match browser_mode {
        FileBrowserMode::Directory => " Select Directory ".to_string(),
        FileBrowserMode::DownloadLocSelection { .. } => String::new(),
        FileBrowserMode::ConfigPathSelection { .. } => " Select Config Path ".to_string(),
        FileBrowserMode::File(exts) => format!(" Select File [{}] ", exts.join(", ")),
    };

    let visible_items = TreeMathHelper::get_visible_slice(data, state, filter, inner_height);
    let mut list_items = Vec::new();

    if data.is_empty() {
        list_items.push(ListItem::new(Line::from(vec![Span::styled(
            "   (Directory is empty)",
            ctx.apply(Style::default().fg(ctx.theme.semantic.overlay0))
                .italic(),
        )])));
    } else if visible_items.is_empty() {
        list_items.push(ListItem::new(Line::from(vec![Span::styled(
            format!("   (No matching files among {} items)", item_count),
            ctx.apply(Style::default().fg(ctx.theme.semantic.overlay0))
                .italic(),
        )])));
    } else {
        for item in visible_items {
            let is_cursor = item.is_cursor;
            let indent_str = "  ".repeat(item.depth);
            let indent_len = indent_str.len();
            let icon_str = if item.node.is_dir {
                ASCII_TREE_DIR_ICON
            } else {
                ASCII_TREE_FILE_ICON
            };
            let icon_len = ASCII_TREE_DIR_ICON.len();

            let (meta_str, meta_len) = if !item.node.is_dir {
                let datetime: chrono::DateTime<chrono::Local> = item.node.payload.modified.into();
                let size_str = format_bytes(item.node.payload.size);
                let s = format!(" {} ({})", size_str, datetime.format("%b %d %H:%M"));
                (s.clone(), s.len())
            } else {
                (String::new(), 0)
            };

            let fixed_used = indent_len + icon_len + meta_len + 1;
            let available_for_name = list_width.saturating_sub(fixed_used);
            let clean_name: String = item
                .node
                .name
                .chars()
                .map(|c| if c.is_control() { '?' } else { c })
                .collect();
            let display_name = truncate_with_ellipsis(&clean_name, available_for_name);

            let (icon_style, text_style) = if is_cursor {
                (
                    Style::default()
                        .fg(ctx.state_warning())
                        .add_modifier(Modifier::BOLD),
                    Style::default()
                        .fg(ctx.state_warning())
                        .add_modifier(Modifier::BOLD),
                )
            } else {
                let i_style = if item.node.is_dir {
                    ctx.apply(Style::default().fg(ctx.state_info()))
                } else {
                    ctx.apply(Style::default().fg(ctx.theme.semantic.text))
                };
                (
                    i_style,
                    ctx.apply(Style::default().fg(ctx.theme.semantic.text)),
                )
            };

            let mut line_spans = vec![
                Span::raw(indent_str),
                Span::styled(icon_str, icon_style),
                Span::styled(display_name, text_style),
            ];

            if !item.node.is_dir {
                line_spans.push(Span::raw(" "));
                line_spans.push(Span::styled(
                    meta_str,
                    ctx.apply(Style::default().fg(ctx.theme.semantic.surface2))
                        .italic(),
                ));
            }

            list_items.push(ListItem::new(Line::from(line_spans)));
        }
    }

    f.render_widget(
        List::new(list_items)
            .block(
                Block::default()
                    .title_top(
                        Line::from(Span::styled(
                            left_title,
                            Style::default().fg(ctx.state_selected()).bold(),
                        ))
                        .alignment(Alignment::Left),
                    )
                    .title_top(
                        Line::from(Span::styled(
                            right_title,
                            Style::default().fg(ctx.state_selected()).italic(),
                        ))
                        .alignment(Alignment::Right),
                    )
                    .borders(Borders::ALL)
                    .border_style(files_border_style),
            )
            .highlight_symbol("▶ "),
        layout.list,
    );
}

fn draw_torrent_preview_panel(
    f: &mut Frame,
    ctx: &ThemeContext,
    area: Rect,
    path: Option<&Path>,
    browser_mode: &FileBrowserMode,
    border_style: Style,
    current_fs_path: &Path,
) {
    let is_narrow = area.width < 50;
    let raw_title = "Torrent Preview";
    let avail_width = area.width.saturating_sub(4) as usize;
    let title = if is_narrow {
        truncate_with_ellipsis("Preview", avail_width)
    } else {
        truncate_with_ellipsis(raw_title, avail_width)
    };

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(border_style)
        .title(title);

    let inner_area = block.inner(area);
    f.render_widget(block, area);

    if let FileBrowserMode::DownloadLocSelection {
        preview_tree,
        preview_state,
        container_name,
        use_container,
        is_editing_name,
        cursor_pos,
        ..
    } = browser_mode
    {
        let header_lines = if *use_container { 2 } else { 1 };
        let list_height = inner_area.height.saturating_sub(header_lines) as usize;

        let visible_rows = TreeMathHelper::get_visible_slice(
            preview_tree,
            preview_state,
            TreeFilter::default(),
            list_height,
        );

        let mut list_items = Vec::new();
        let root_style = Style::default()
            .fg(ctx.state_info())
            .add_modifier(Modifier::BOLD);

        let path_display = if is_narrow {
            current_fs_path
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_else(|| "/".to_string())
        } else {
            current_fs_path.to_string_lossy().to_string()
        };

        list_items.push(ListItem::new(Line::from(vec![
            Span::styled(ASCII_TREE_ROOT_ICON, root_style),
            Span::styled(path_display, root_style),
        ])));

        if *use_container {
            let container_style = if *is_editing_name {
                Style::default()
                    .fg(ctx.accent_sky())
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default()
                    .fg(ctx.state_selected())
                    .add_modifier(Modifier::BOLD)
            };

            let mut spans = vec![
                Span::raw("  "),
                Span::styled(ASCII_TREE_ROOT_ICON, container_style),
            ];

            if *is_editing_name {
                let (before, after) = container_name.split_at(*cursor_pos);
                spans.push(Span::styled(before, container_style));
                spans.push(Span::styled(
                    "█",
                    Style::default()
                        .fg(ctx.accent_sky())
                        .add_modifier(Modifier::SLOW_BLINK),
                ));
                spans.push(Span::styled(after, container_style));
            } else {
                spans.push(Span::styled(container_name.clone(), container_style));
                if !is_narrow {
                    spans.push(Span::styled(
                        " (New)",
                        Style::default()
                            .fg(ctx.theme.semantic.surface2)
                            .add_modifier(Modifier::ITALIC),
                    ));
                }
            }
            list_items.push(ListItem::new(Line::from(spans)));
        }

        let tree_items: Vec<ListItem> = visible_rows
            .iter()
            .map(|item| {
                let is_cursor = item.is_cursor;
                let base_indent_level = if *use_container { 2 } else { 1 };
                let indent_multiplier = if is_narrow { 1 } else { 2 };
                let indent_str = " ".repeat((base_indent_level + item.depth) * indent_multiplier);

                let icon = if item.node.is_dir {
                    ASCII_TREE_DIR_ICON
                } else {
                    ASCII_TREE_FILE_ICON
                };

                let (base_content_style, tag) = match item.node.payload.priority {
                    FilePriority::Skip => (
                        Style::default()
                            .fg(ctx.theme.semantic.surface1)
                            .add_modifier(Modifier::CROSSED_OUT),
                        "[S] ",
                    ),
                    FilePriority::High => (
                        Style::default()
                            .fg(ctx.state_success())
                            .add_modifier(Modifier::BOLD),
                        "[H] ",
                    ),
                    FilePriority::Mixed => (
                        Style::default()
                            .fg(ctx.state_warning())
                            .add_modifier(Modifier::ITALIC),
                        "[*] ",
                    ),
                    FilePriority::Normal => (
                        if item.node.is_dir {
                            ctx.apply(Style::default().fg(ctx.state_info()))
                        } else {
                            ctx.apply(Style::default().fg(ctx.theme.semantic.text))
                        },
                        "",
                    ),
                };

                let final_content_style = if is_cursor {
                    base_content_style
                        .add_modifier(Modifier::BOLD)
                        .add_modifier(Modifier::UNDERLINED)
                } else {
                    base_content_style
                };

                let structure_style = final_content_style
                    .remove_modifier(Modifier::CROSSED_OUT)
                    .remove_modifier(Modifier::UNDERLINED);
                let mut spans = vec![
                    Span::styled(indent_str, structure_style),
                    Span::styled(icon, structure_style),
                    Span::styled(&item.node.name, final_content_style),
                ];

                if !item.node.is_dir {
                    if !is_narrow {
                        spans.push(Span::styled(
                            format!(" ({}) ", format_bytes(item.node.payload.size)),
                            structure_style,
                        ));
                    }
                    if !tag.is_empty() {
                        spans.push(Span::styled(tag, structure_style));
                    }
                }
                ListItem::new(Line::from(spans))
            })
            .collect();

        list_items.extend(tree_items);
        f.render_widget(List::new(list_items), inner_area);
        return;
    }

    if let Some(p) = path {
        let file_bytes = match std::fs::read(p) {
            Ok(b) => b,
            Err(e) => {
                f.render_widget(
                    Paragraph::new(format!("Read Error: {}", e))
                        .style(ctx.apply(Style::default().fg(ctx.state_error()))),
                    inner_area,
                );
                return;
            }
        };

        let torrent = match crate::torrent_file::parser::from_bytes(&file_bytes) {
            Ok(t) => t,
            Err(e) => {
                f.render_widget(
                    Paragraph::new(format!("Invalid Torrent: {}", e))
                        .style(ctx.apply(Style::default().fg(ctx.state_error()))),
                    inner_area,
                );
                return;
            }
        };

        let total_size = torrent.info.total_length();
        let protocol_version = match torrent.info.meta_version {
            Some(2) => {
                if !torrent.info.pieces.is_empty() {
                    "BitTorrent v2 (Hybrid)"
                } else {
                    "BitTorrent v2 (Pure)"
                }
            }
            _ => "BitTorrent v1",
        };
        let info_text = vec![
            Line::from(vec![
                Span::styled(
                    "Name: ",
                    ctx.apply(Style::default().fg(ctx.theme.semantic.subtext0)),
                ),
                Span::raw(&torrent.info.name),
            ]),
            Line::from(vec![
                Span::styled(
                    "Protocol: ",
                    ctx.apply(Style::default().fg(ctx.theme.semantic.subtext0)),
                ),
                Span::styled(
                    protocol_version,
                    Style::default().fg(ctx.state_selected()).bold(),
                ),
            ]),
            Line::from(vec![
                Span::styled(
                    "Size: ",
                    ctx.apply(Style::default().fg(ctx.theme.semantic.subtext0)),
                ),
                Span::raw(format_bytes(total_size as u64)),
            ]),
        ];

        let layout = Layout::vertical([
            Constraint::Length(info_text.len() as u16 + 1),
            Constraint::Min(0),
        ])
        .split(inner_area);
        f.render_widget(
            Paragraph::new(info_text).block(
                Block::default()
                    .borders(Borders::BOTTOM)
                    .border_style(ctx.apply(Style::default().fg(ctx.theme.semantic.border))),
            ),
            layout[0],
        );

        let file_list_payloads: Vec<(Vec<String>, TorrentPreviewPayload)> = torrent
            .file_list()
            .into_iter()
            .map(|(path, size)| {
                (
                    path,
                    TorrentPreviewPayload {
                        file_index: None,
                        size,
                        priority: FilePriority::Normal,
                    },
                )
            })
            .collect();

        let final_nodes = RawNode::from_path_list(None, file_list_payloads);
        let mut temp_state = TreeViewState::default();
        for node in &final_nodes {
            node.expand_all(&mut temp_state);
        }

        let visible_rows = TreeMathHelper::get_visible_slice(
            &final_nodes,
            &temp_state,
            TreeFilter::default(),
            layout[1].height as usize,
        );

        let list_items: Vec<ListItem> = visible_rows
            .iter()
            .map(|item| {
                let indent = if is_narrow {
                    " ".repeat(item.depth)
                } else {
                    "  ".repeat(item.depth)
                };
                let icon = if item.node.is_dir {
                    ASCII_TREE_DIR_ICON
                } else {
                    ASCII_TREE_FILE_ICON
                };
                let style = if item.node.is_dir {
                    ctx.apply(Style::default().fg(ctx.state_info()))
                } else {
                    ctx.apply(Style::default().fg(ctx.theme.semantic.text))
                };
                let mut spans = vec![
                    Span::raw(indent),
                    Span::styled(icon, style),
                    Span::styled(&item.node.name, style),
                ];
                if !item.node.is_dir && !is_narrow {
                    spans.push(Span::styled(
                        format!(" ({})", format_bytes(item.node.payload.size)),
                        ctx.apply(Style::default().fg(ctx.theme.semantic.surface2)),
                    ));
                }
                ListItem::new(Line::from(spans))
            })
            .collect();

        f.render_widget(List::new(list_items), layout[1]);
    }
}

pub async fn handle_event(event: CrosstermEvent, app: &mut App) {
    if !matches!(app.app_state.mode, AppMode::FileBrowser) {
        return;
    }

    if let CrosstermEvent::Key(key) = event {
        if key.kind == KeyEventKind::Press {
            if handle_browser_search_key(key.code, app) {
                return;
            }

            if handle_browser_download_key(key.code, app).await {
                return;
            }

            let _ = handle_browser_common_key(key.code, app).await;
        }
    }
}

fn handle_browser_search_key(key_code: KeyCode, app: &mut App) -> bool {
    if let Some(action) =
        map_search_key_to_browser_action(key_code, app.app_state.ui.file_browser.is_searching)
    {
        let reduced = reduce_browser_action(
            action,
            &mut app.app_state.ui.file_browser.is_searching,
            &mut app.app_state.ui.file_browser.search_query,
        );
        if reduced.redraw {
            app.app_state.ui.needs_redraw = true;
        }
        return true;
    }
    false
}

async fn handle_browser_download_key(key_code: KeyCode, app: &mut App) -> bool {
    let consumed_download_input = {
        let browser_mode = &mut app.app_state.ui.file_browser.browser_mode;
        if let Some(action) = map_download_key_to_action(key_code, browser_mode) {
            reduce_browser_download_action(action, browser_mode).consumed
        } else {
            false
        }
    };
    if consumed_download_input {
        return true;
    }

    if !matches!(
        app.app_state.ui.file_browser.browser_mode,
        FileBrowserMode::DownloadLocSelection { .. }
    ) {
        return false;
    }

    if key_code == KeyCode::Esc {
        let reduced = {
            let file_browser = &app.app_state.ui.file_browser;
            reduce_browser_dialog_action(
                BrowserDialogAction::CancelDownloadSelection,
                &file_browser.state,
                &file_browser.browser_mode,
                !app.app_state.pending_torrent_link.is_empty(),
            )
        };
        execute_browser_dialog_effects(app, reduced.effects).await;
        return true;
    }

    let screen_area = app.app_state.screen_area;
    let is_searching = app.app_state.ui.file_browser.is_searching;
    let consumed_preview_input = {
        let browser_mode = &mut app.app_state.ui.file_browser.browser_mode;
        if let FileBrowserMode::DownloadLocSelection {
            use_container,
            focused_pane,
            preview_tree,
            preview_state,
            ..
        } = browser_mode
        {
            if matches!(focused_pane, BrowserPane::TorrentPreview) {
                let list_height = calculate_preview_list_height(
                    screen_area,
                    is_searching,
                    focused_pane,
                    *use_container,
                );
                reduce_browser_preview_action(
                    map_preview_key_to_action(key_code),
                    preview_state,
                    preview_tree,
                    list_height,
                )
                .consumed
            } else {
                false
            }
        } else {
            false
        }
    };
    if consumed_preview_input {
        return true;
    }

    false
}

async fn handle_browser_common_key(key_code: KeyCode, app: &mut App) -> bool {
    let list_height = {
        let file_browser = &app.app_state.ui.file_browser;
        let has_preview = has_preview_content(
            &file_browser.browser_mode,
            app.app_state.pending_torrent_path.is_some(),
            !app.app_state.pending_torrent_link.is_empty(),
            file_browser.state.cursor_path.as_ref(),
        );
        let pane = focused_pane(&file_browser.browser_mode);
        calculate_list_height(
            app.app_state.screen_area,
            has_preview,
            app.app_state.ui.file_browser.is_searching,
            &pane,
        )
    };

    let consumed_filesystem = {
        let file_browser = &mut app.app_state.ui.file_browser;
        handle_filesystem_navigation(
            key_code,
            BrowserFilesystemNavContext {
                state: &mut file_browser.state,
                data: &file_browser.data,
                browser_mode: &file_browser.browser_mode,
                is_searching: &mut file_browser.is_searching,
                search_query: &mut file_browser.search_query,
                list_height,
                app_command_tx: &app.app_command_tx,
            },
        )
    };
    if consumed_filesystem {
        return true;
    }

    let dialog_action = match key_code {
        KeyCode::Char('Y') => Some(BrowserDialogAction::ConfirmSelection),
        KeyCode::Esc => Some(BrowserDialogAction::Escape),
        _ => None,
    };
    let Some(dialog_action) = dialog_action else {
        return false;
    };

    let reduced = {
        let file_browser = &app.app_state.ui.file_browser;
        reduce_browser_dialog_action(
            dialog_action,
            &file_browser.state,
            &file_browser.browser_mode,
            !app.app_state.pending_torrent_link.is_empty(),
        )
    };
    execute_browser_dialog_effects(app, reduced.effects).await;
    true
}

pub enum ConfirmDecision {
    ToConfig(ConfigUiState),
    Download(DownloadConfirmPayload),
    File(PathBuf),
    None,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserAction {
    Esc,
    Enter,
    Backspace,
    Char(char),
    Noop,
}

pub struct BrowserReduceResult {
    pub redraw: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserFsAction {
    StartSearch,
    Move(TreeAction),
    EnterDir,
    GoParent,
}

pub enum BrowserFsEffect {
    FetchFileTree {
        path: PathBuf,
        browser_mode: FileBrowserMode,
        highlight_path: Option<PathBuf>,
    },
}

pub struct BrowserFsReduceResult {
    pub consumed: bool,
    pub effects: Vec<BrowserFsEffect>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDialogAction {
    ConfirmSelection,
    Escape,
    CancelDownloadSelection,
}

pub enum BrowserDialogEffect {
    ExecuteConfirmDecision(ConfirmDecision),
    ToConfig(ConfigUiState),
    CleanupPendingLink { async_delete: bool },
    ToNormalAndClearPending,
    ClearSearch,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserTransition {
    ToNormal,
    ToConfig,
}

pub struct BrowserDialogReduceResult {
    pub effects: Vec<BrowserDialogEffect>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDownloadEditAction {
    Commit,
    Cancel,
    MoveLeft,
    MoveRight,
    Backspace,
    Delete,
    Insert(char),
    Noop,
}

pub struct BrowserDownloadEditReduceResult {
    pub consumed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDownloadShortcutAction {
    ToggleUseContainer,
    StartRename,
    TogglePane,
}

pub struct BrowserDownloadShortcutReduceResult {
    pub consumed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDownloadAction {
    Edit(BrowserDownloadEditAction),
    Shortcut(BrowserDownloadShortcutAction),
}

pub struct BrowserDownloadReduceResult {
    pub consumed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserPreviewAction {
    ConfirmSelection,
    Navigate(TreeAction),
    CyclePriority,
    Ignore,
}

pub struct BrowserPreviewReduceResult {
    pub consumed: bool,
}

pub struct BrowserFilesystemNavContext<'a> {
    pub state: &'a mut TreeViewState,
    pub data: &'a [RawNode<FileMetadata>],
    pub browser_mode: &'a FileBrowserMode,
    pub is_searching: &'a mut bool,
    pub search_query: &'a mut String,
    pub list_height: usize,
    pub app_command_tx: &'a mpsc::Sender<AppCommand>,
}

fn map_search_key_to_browser_action(
    key_code: KeyCode,
    is_searching: bool,
) -> Option<BrowserAction> {
    if !is_searching {
        return None;
    }

    Some(match key_code {
        KeyCode::Esc => BrowserAction::Esc,
        KeyCode::Enter => BrowserAction::Enter,
        KeyCode::Backspace => BrowserAction::Backspace,
        KeyCode::Char(c) => BrowserAction::Char(c),
        _ => BrowserAction::Noop,
    })
}

pub fn reduce_browser_action(
    action: BrowserAction,
    is_searching: &mut bool,
    search_query: &mut String,
) -> BrowserReduceResult {
    match action {
        BrowserAction::Esc => {
            *is_searching = false;
            search_query.clear();
        }
        BrowserAction::Enter => {
            *is_searching = false;
        }
        BrowserAction::Backspace => {
            search_query.pop();
        }
        BrowserAction::Char(c) => {
            search_query.push(c);
        }
        BrowserAction::Noop => {}
    }

    BrowserReduceResult { redraw: true }
}

fn map_filesystem_key_to_action(key_code: KeyCode) -> Option<BrowserFsAction> {
    match key_code {
        KeyCode::Char('/') => Some(BrowserFsAction::StartSearch),
        KeyCode::Up | KeyCode::Char('k') => Some(BrowserFsAction::Move(TreeAction::Up)),
        KeyCode::Down | KeyCode::Char('j') => Some(BrowserFsAction::Move(TreeAction::Down)),
        KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => Some(BrowserFsAction::EnterDir),
        KeyCode::Backspace | KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('u') => {
            Some(BrowserFsAction::GoParent)
        }
        _ => None,
    }
}

pub fn reduce_filesystem_navigation_action(
    action: BrowserFsAction,
    state: &mut TreeViewState,
    data: &[RawNode<FileMetadata>],
    browser_mode: &FileBrowserMode,
    is_searching: &mut bool,
    search_query: &mut String,
    list_height: usize,
) -> BrowserFsReduceResult {
    let filter = build_filter(browser_mode, search_query);
    let mut result = BrowserFsReduceResult {
        consumed: true,
        effects: Vec::new(),
    };

    match action {
        BrowserFsAction::StartSearch => {
            *is_searching = true;
            search_query.clear();
        }
        BrowserFsAction::Move(tree_action) => {
            TreeMathHelper::apply_action(state, data, tree_action, filter, list_height);
        }
        BrowserFsAction::EnterDir => {
            if let Some(path) = state.cursor_path.clone() {
                if path.is_dir() {
                    *is_searching = false;
                    search_query.clear();
                    result.effects.push(BrowserFsEffect::FetchFileTree {
                        path,
                        browser_mode: browser_mode.clone(),
                        highlight_path: None,
                    });
                }
            }
        }
        BrowserFsAction::GoParent => {
            let child_to_highlight = state.current_path.clone();
            if let Some(parent) = state.current_path.parent() {
                result.effects.push(BrowserFsEffect::FetchFileTree {
                    path: parent.to_path_buf(),
                    browser_mode: browser_mode.clone(),
                    highlight_path: Some(child_to_highlight),
                });
            }
        }
    }

    result
}

fn map_download_name_edit_key_to_action(key_code: KeyCode) -> BrowserDownloadEditAction {
    match key_code {
        KeyCode::Enter => BrowserDownloadEditAction::Commit,
        KeyCode::Esc => BrowserDownloadEditAction::Cancel,
        KeyCode::Left => BrowserDownloadEditAction::MoveLeft,
        KeyCode::Right => BrowserDownloadEditAction::MoveRight,
        KeyCode::Backspace => BrowserDownloadEditAction::Backspace,
        KeyCode::Delete => BrowserDownloadEditAction::Delete,
        KeyCode::Char(c) => BrowserDownloadEditAction::Insert(c),
        _ => BrowserDownloadEditAction::Noop,
    }
}

pub fn map_download_key_to_action(
    key_code: KeyCode,
    browser_mode: &FileBrowserMode,
) -> Option<BrowserDownloadAction> {
    if let FileBrowserMode::DownloadLocSelection {
        is_editing_name,
        use_container,
        ..
    } = browser_mode
    {
        if *is_editing_name {
            return Some(BrowserDownloadAction::Edit(
                map_download_name_edit_key_to_action(key_code),
            ));
        }

        if let Some(action) = map_download_shortcut_key_to_action(key_code, *use_container) {
            return Some(BrowserDownloadAction::Shortcut(action));
        }
    }
    None
}

pub fn reduce_download_name_edit_action(
    action: BrowserDownloadEditAction,
    container_name: &mut String,
    is_editing_name: &mut bool,
    cursor_pos: &mut usize,
    original_name_backup: &str,
) -> BrowserDownloadEditReduceResult {
    match action {
        BrowserDownloadEditAction::Commit => {
            *is_editing_name = false;
        }
        BrowserDownloadEditAction::Cancel => {
            *container_name = original_name_backup.to_string();
            *is_editing_name = false;
            *cursor_pos = container_name.len();
        }
        BrowserDownloadEditAction::MoveLeft => {
            *cursor_pos = cursor_pos.saturating_sub(1);
        }
        BrowserDownloadEditAction::MoveRight => {
            if *cursor_pos < container_name.len() {
                *cursor_pos += 1;
            }
        }
        BrowserDownloadEditAction::Backspace => {
            if *cursor_pos > 0 {
                container_name.remove(*cursor_pos - 1);
                *cursor_pos -= 1;
            }
        }
        BrowserDownloadEditAction::Delete => {
            if *cursor_pos < container_name.len() {
                container_name.remove(*cursor_pos);
            }
        }
        BrowserDownloadEditAction::Insert(c) => {
            container_name.insert(*cursor_pos, c);
            *cursor_pos += 1;
        }
        BrowserDownloadEditAction::Noop => {}
    }

    BrowserDownloadEditReduceResult { consumed: true }
}

fn map_download_shortcut_key_to_action(
    key_code: KeyCode,
    use_container: bool,
) -> Option<BrowserDownloadShortcutAction> {
    match key_code {
        KeyCode::Char('x') => Some(BrowserDownloadShortcutAction::ToggleUseContainer),
        KeyCode::Char('r') if use_container => Some(BrowserDownloadShortcutAction::StartRename),
        KeyCode::Tab => Some(BrowserDownloadShortcutAction::TogglePane),
        _ => None,
    }
}

pub fn reduce_download_shortcut_action(
    action: BrowserDownloadShortcutAction,
    container_name: &str,
    use_container: &mut bool,
    is_editing_name: &mut bool,
    focused_pane: &mut BrowserPane,
    cursor_pos: &mut usize,
    original_name_backup: &mut String,
) -> BrowserDownloadShortcutReduceResult {
    match action {
        BrowserDownloadShortcutAction::ToggleUseContainer => {
            *use_container = !*use_container;
        }
        BrowserDownloadShortcutAction::StartRename => {
            *is_editing_name = true;
            *original_name_backup = container_name.to_string();
            *cursor_pos = container_name.len();
            *focused_pane = BrowserPane::TorrentPreview;
        }
        BrowserDownloadShortcutAction::TogglePane => {
            *focused_pane = match focused_pane {
                BrowserPane::FileSystem => BrowserPane::TorrentPreview,
                BrowserPane::TorrentPreview => BrowserPane::FileSystem,
            };
        }
    }

    BrowserDownloadShortcutReduceResult { consumed: true }
}

pub fn reduce_browser_download_action(
    action: BrowserDownloadAction,
    browser_mode: &mut FileBrowserMode,
) -> BrowserDownloadReduceResult {
    if let FileBrowserMode::DownloadLocSelection {
        container_name,
        use_container,
        is_editing_name,
        focused_pane,
        cursor_pos,
        original_name_backup,
        ..
    } = browser_mode
    {
        let consumed = match action {
            BrowserDownloadAction::Edit(edit_action) => {
                reduce_download_name_edit_action(
                    edit_action,
                    container_name,
                    is_editing_name,
                    cursor_pos,
                    original_name_backup,
                )
                .consumed
            }
            BrowserDownloadAction::Shortcut(shortcut_action) => {
                reduce_download_shortcut_action(
                    shortcut_action,
                    container_name,
                    use_container,
                    is_editing_name,
                    focused_pane,
                    cursor_pos,
                    original_name_backup,
                )
                .consumed
            }
        };

        return BrowserDownloadReduceResult { consumed };
    }

    BrowserDownloadReduceResult { consumed: false }
}

pub fn has_preview_content(
    browser_mode: &FileBrowserMode,
    pending_torrent_path: bool,
    pending_torrent_link: bool,
    cursor_path: Option<&std::path::PathBuf>,
) -> bool {
    match browser_mode {
        FileBrowserMode::DownloadLocSelection { .. } => {
            pending_torrent_path || pending_torrent_link
        }
        FileBrowserMode::File(_) => {
            cursor_path.is_some_and(|p| p.extension().is_some_and(|ext| ext == "torrent"))
        }
        _ => false,
    }
}

pub fn focused_pane(browser_mode: &FileBrowserMode) -> BrowserPane {
    if let FileBrowserMode::DownloadLocSelection { focused_pane, .. } = browser_mode {
        focused_pane.clone()
    } else {
        BrowserPane::FileSystem
    }
}

pub fn calculate_area(screen: Rect, has_preview_content: bool) -> Rect {
    if has_preview_content {
        if screen.width < 60 {
            screen
        } else {
            centered_rect(90, 80, screen)
        }
    } else if screen.width < 40 {
        screen
    } else {
        centered_rect(75, 80, screen)
    }
}

pub fn calculate_list_height(
    screen: Rect,
    has_preview_content: bool,
    is_searching: bool,
    focused_pane: &BrowserPane,
) -> usize {
    let area = calculate_area(screen, has_preview_content);
    let layout =
        calculate_file_browser_layout(area, has_preview_content, is_searching, focused_pane);
    layout.list.height.saturating_sub(2) as usize
}

pub fn calculate_preview_list_height(
    screen: Rect,
    is_searching: bool,
    focused_pane: &BrowserPane,
    use_container: bool,
) -> Option<usize> {
    let area = if screen.width < 60 {
        screen
    } else {
        centered_rect(90, 80, screen)
    };
    let layout = calculate_file_browser_layout(area, true, is_searching, focused_pane);
    layout.preview.map(|preview_rect| {
        let inner_height = preview_rect.height.saturating_sub(2);
        let header_rows = if use_container { 2 } else { 1 };
        inner_height.saturating_sub(header_rows) as usize
    })
}

pub fn map_preview_key_to_action(key_code: KeyCode) -> BrowserPreviewAction {
    match key_code {
        KeyCode::Char('Y') => BrowserPreviewAction::ConfirmSelection,
        KeyCode::Up | KeyCode::Char('k') => BrowserPreviewAction::Navigate(TreeAction::Up),
        KeyCode::Down | KeyCode::Char('j') => BrowserPreviewAction::Navigate(TreeAction::Down),
        KeyCode::Left | KeyCode::Char('h') => BrowserPreviewAction::Navigate(TreeAction::Left),
        KeyCode::Right | KeyCode::Char('l') => BrowserPreviewAction::Navigate(TreeAction::Right),
        KeyCode::Char(' ') => BrowserPreviewAction::CyclePriority,
        _ => BrowserPreviewAction::Ignore,
    }
}

pub fn reduce_browser_preview_action(
    action: BrowserPreviewAction,
    preview_state: &mut TreeViewState,
    preview_tree: &mut [RawNode<TorrentPreviewPayload>],
    list_height: Option<usize>,
) -> BrowserPreviewReduceResult {
    match action {
        BrowserPreviewAction::ConfirmSelection => BrowserPreviewReduceResult { consumed: false },
        BrowserPreviewAction::Navigate(tree_action) => {
            if let Some(height) = list_height {
                TreeMathHelper::apply_action(
                    preview_state,
                    preview_tree,
                    tree_action,
                    TreeFilter::default(),
                    height,
                );
            }
            BrowserPreviewReduceResult { consumed: true }
        }
        BrowserPreviewAction::CyclePriority => {
            if let Some(_height) = list_height {
                if let Some(target) = &preview_state.cursor_path {
                    apply_priority_cycle(preview_tree, target);
                }
            }
            BrowserPreviewReduceResult { consumed: true }
        }
        BrowserPreviewAction::Ignore => BrowserPreviewReduceResult { consumed: true },
    }
}

pub fn build_filter(
    browser_mode: &FileBrowserMode,
    search_query: &str,
) -> TreeFilter<FileMetadata> {
    match browser_mode {
        FileBrowserMode::Directory
        | FileBrowserMode::DownloadLocSelection { .. }
        | FileBrowserMode::ConfigPathSelection { .. } => TreeFilter::from_text(search_query),
        FileBrowserMode::File(extensions) => {
            let exts = extensions.clone();
            TreeFilter::new(search_query, move |node| {
                node.is_dir || exts.iter().any(|ext| node.name.ends_with(ext))
            })
        }
    }
}

pub fn handle_filesystem_navigation(
    key_code: KeyCode,
    ctx: BrowserFilesystemNavContext<'_>,
) -> bool {
    if let Some(action) = map_filesystem_key_to_action(key_code) {
        let reduced = reduce_filesystem_navigation_action(
            action,
            ctx.state,
            ctx.data,
            ctx.browser_mode,
            ctx.is_searching,
            ctx.search_query,
            ctx.list_height,
        );
        for effect in reduced.effects {
            match effect {
                BrowserFsEffect::FetchFileTree {
                    path,
                    browser_mode,
                    highlight_path,
                } => {
                    let _ = ctx.app_command_tx.try_send(AppCommand::FetchFileTree {
                        path,
                        browser_mode,
                        highlight_path,
                    });
                }
            }
        }
        reduced.consumed
    } else {
        false
    }
}

pub fn reduce_browser_dialog_action(
    action: BrowserDialogAction,
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
    has_pending_torrent_link: bool,
) -> BrowserDialogReduceResult {
    let mut result = BrowserDialogReduceResult {
        effects: Vec::new(),
    };

    match action {
        BrowserDialogAction::ConfirmSelection => {
            result
                .effects
                .push(BrowserDialogEffect::ExecuteConfirmDecision(
                    resolve_confirm_decision(state, browser_mode),
                ));
            result.effects.push(BrowserDialogEffect::ClearSearch);
        }
        BrowserDialogAction::Escape => {
            if let Some(config_ui) = escape_to_config_mode(browser_mode) {
                result.effects.push(BrowserDialogEffect::ClearSearch);
                result
                    .effects
                    .push(BrowserDialogEffect::ToConfig(config_ui));
                return result;
            }

            if matches!(browser_mode, FileBrowserMode::DownloadLocSelection { .. })
                && has_pending_torrent_link
            {
                result
                    .effects
                    .push(BrowserDialogEffect::CleanupPendingLink {
                        async_delete: false,
                    });
            }

            result.effects.push(BrowserDialogEffect::ClearSearch);
            result
                .effects
                .push(BrowserDialogEffect::ToNormalAndClearPending);
        }
        BrowserDialogAction::CancelDownloadSelection => {
            if has_pending_torrent_link {
                result
                    .effects
                    .push(BrowserDialogEffect::CleanupPendingLink { async_delete: true });
            }
            result.effects.push(BrowserDialogEffect::ClearSearch);
            result
                .effects
                .push(BrowserDialogEffect::ToNormalAndClearPending);
        }
    }

    result
}

pub async fn execute_browser_dialog_effects(app: &mut App, effects: Vec<BrowserDialogEffect>) {
    for effect in effects {
        match effect {
            BrowserDialogEffect::ExecuteConfirmDecision(decision) => {
                if let Some(transition) = execute_confirm_decision(app, decision).await {
                    apply_browser_transition(app, transition);
                }
            }
            BrowserDialogEffect::ToConfig(config_ui) => {
                app.app_state.ui.config = config_ui;
                apply_browser_transition(app, BrowserTransition::ToConfig);
            }
            BrowserDialogEffect::CleanupPendingLink { async_delete } => {
                cleanup_pending_link_on_escape(
                    &app.app_state.pending_torrent_link,
                    &mut app.torrent_manager_command_txs,
                    &mut app.app_state.torrents,
                    &mut app.app_state.torrent_list_order,
                    async_delete,
                );
            }
            BrowserDialogEffect::ToNormalAndClearPending => {
                apply_browser_transition(app, BrowserTransition::ToNormal);
                app.app_state.pending_torrent_path = None;
                app.app_state.pending_torrent_link.clear();
            }
            BrowserDialogEffect::ClearSearch => {
                app.app_state.ui.file_browser.is_searching = false;
                app.app_state.ui.file_browser.search_query.clear();
            }
        }
    }
}

fn apply_browser_transition(app: &mut App, transition: BrowserTransition) {
    match transition {
        BrowserTransition::ToNormal => app.app_state.mode = AppMode::Normal,
        BrowserTransition::ToConfig => app.app_state.mode = AppMode::Config,
    }
}

pub fn confirm_config_path_selection(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> Option<ConfigUiState> {
    if let FileBrowserMode::ConfigPathSelection {
        target_item,
        current_settings,
        selected_index,
        items,
    } = browser_mode
    {
        let mut new_settings = current_settings.clone();
        let selected_path = state.current_path.clone();

        match target_item {
            ConfigItem::DefaultDownloadFolder => {
                if !crate::config::is_shared_config_mode() {
                    new_settings.default_download_folder = Some(selected_path)
                }
            }
            ConfigItem::WatchFolder => new_settings.watch_folder = Some(selected_path),
            _ => {}
        }

        return Some(ConfigUiState {
            settings_edit: new_settings,
            selected_index: *selected_index,
            items: items.clone(),
            editing: None,
        });
    }
    None
}

pub fn escape_to_config_mode(browser_mode: &FileBrowserMode) -> Option<ConfigUiState> {
    if let FileBrowserMode::ConfigPathSelection {
        current_settings,
        selected_index,
        items,
        ..
    } = browser_mode
    {
        return Some(ConfigUiState {
            settings_edit: current_settings.clone(),
            selected_index: *selected_index,
            items: items.clone(),
            editing: None,
        });
    }
    None
}

pub fn selected_torrent_file_for_confirm(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> Option<std::path::PathBuf> {
    if let FileBrowserMode::File(extensions) = browser_mode {
        if let Some(path) = state.cursor_path.clone() {
            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
            if extensions.iter().any(|ext| name.ends_with(ext)) {
                return Some(path);
            }
        }
    }
    None
}

pub fn resolve_confirm_decision(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> ConfirmDecision {
    if let Some(config_ui) = confirm_config_path_selection(state, browser_mode) {
        return ConfirmDecision::ToConfig(config_ui);
    }
    if let Some(payload) = build_download_confirm_payload(state, browser_mode) {
        return ConfirmDecision::Download(payload);
    }
    if let Some(path) = selected_torrent_file_for_confirm(state, browser_mode) {
        return ConfirmDecision::File(path);
    }
    ConfirmDecision::None
}

pub async fn execute_confirm_decision(
    app: &mut App,
    decision: ConfirmDecision,
) -> Option<BrowserTransition> {
    match decision {
        ConfirmDecision::ToConfig(config_ui) => {
            tracing::info!(target: "superseedr", "Confirming Config Path Selection");
            app.app_state.ui.config = config_ui;
            Some(BrowserTransition::ToConfig)
        }
        ConfirmDecision::Download(payload) => {
            if let Some(pending_path) = app.app_state.pending_torrent_path.take() {
                match app.prepare_add_torrent_file_request(
                    pending_path,
                    Some(payload.base_path.clone()),
                    payload.container_name_to_use.clone(),
                    payload.file_priorities.clone(),
                ) {
                    Ok(request) => {
                        let _ = app
                            .app_command_tx
                            .send(AppCommand::SubmitControlRequest(request))
                            .await;
                    }
                    Err(error) => {
                        app.app_state.system_error = Some(error);
                    }
                }
            } else if !app.app_state.pending_torrent_link.is_empty() {
                let request = app.prepare_add_magnet_request(
                    app.app_state.pending_torrent_link.clone(),
                    Some(payload.base_path),
                    payload.container_name_to_use,
                    payload.file_priorities,
                );
                let _ = app
                    .app_command_tx
                    .send(AppCommand::SubmitControlRequest(request))
                    .await;
                app.app_state.pending_torrent_link.clear();
            } else {
                tracing::warn!(target: "superseedr", "SHIFT+Y pressed but no pending content was found");
            }
            Some(BrowserTransition::ToNormal)
        }
        ConfirmDecision::File(path) => {
            if path
                .file_name()
                .and_then(|n| n.to_str())
                .is_some_and(|name| name.ends_with(".torrent"))
            {
                let _ = app
                    .app_command_tx
                    .send(AppCommand::AddTorrentFromFile(path))
                    .await;
            }
            Some(BrowserTransition::ToNormal)
        }
        ConfirmDecision::None => None,
    }
}

pub fn build_download_confirm_payload(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> Option<DownloadConfirmPayload> {
    if let FileBrowserMode::DownloadLocSelection {
        container_name,
        use_container,
        preview_tree,
        ..
    } = browser_mode
    {
        let base_path = state.current_path.clone();
        let container_name_to_use = if *use_container {
            Some(container_name.clone())
        } else {
            Some(String::new())
        };

        let mut file_priorities = HashMap::new();
        for node in preview_tree {
            node.collect_priorities(&mut file_priorities);
        }

        return Some(DownloadConfirmPayload {
            base_path,
            container_name_to_use,
            file_priorities,
        });
    }
    None
}

pub fn pending_link_info_hash(pending_torrent_link: &str) -> Option<Vec<u8>> {
    if pending_torrent_link.is_empty() {
        return None;
    }
    crate::app::parse_hybrid_hashes(pending_torrent_link).0
}

pub fn cleanup_pending_link_on_escape(
    pending_torrent_link: &str,
    torrent_manager_command_txs: &mut HashMap<Vec<u8>, Sender<ManagerCommand>>,
    torrents: &mut HashMap<Vec<u8>, TorrentDisplayState>,
    torrent_list_order: &mut Vec<Vec<u8>>,
    async_delete: bool,
) {
    if let Some(info_hash) = pending_link_info_hash(pending_torrent_link) {
        if async_delete {
            if let Some(manager_tx) = torrent_manager_command_txs.get(&info_hash) {
                let tx = manager_tx.clone();
                tokio::spawn(async move {
                    if let Err(e) = tx.send(ManagerCommand::DeleteFile).await {
                        tracing::error!("Failed to send DeleteFile to cancelled manager: {}", e);
                    }
                });
            }
            torrent_manager_command_txs.remove(&info_hash);
        } else if let Some(manager_tx) = torrent_manager_command_txs.remove(&info_hash) {
            let _ = manager_tx.try_send(ManagerCommand::DeleteFile);
        }

        torrents.remove(&info_hash);
        torrent_list_order.retain(|h| h != &info_hash);
    }
}

pub fn apply_priority_cycle(
    nodes: &mut [RawNode<TorrentPreviewPayload>],
    target_path: &Path,
) -> bool {
    for node in nodes {
        let found = node.find_and_act(target_path, &mut |target_node| {
            let new_priority = target_node.payload.priority.next();
            target_node.apply_recursive(&|n| {
                n.payload.priority = new_priority;
            });
        });

        if found {
            return true;
        }
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::{BrowserPane, ConfigItem, TorrentPreviewPayload};
    use crate::tui::tree::{RawNode, TreeViewState};
    use std::path::PathBuf;

    #[test]
    fn search_reducer_clears_on_escape() {
        let mut is_searching = true;
        let mut query = String::from("abc");
        let action = map_search_key_to_browser_action(KeyCode::Esc, is_searching)
            .expect("expected search action");
        let out = reduce_browser_action(action, &mut is_searching, &mut query);
        assert!(out.redraw);
        assert!(!is_searching);
        assert!(query.is_empty());
    }

    #[test]
    fn reducer_search_char_appends_and_consumes() {
        let mut is_searching = true;
        let mut query = String::from("ab");

        let out = reduce_browser_action(BrowserAction::Char('c'), &mut is_searching, &mut query);

        assert!(out.redraw);
        assert!(is_searching);
        assert_eq!(query, "abc");
    }

    #[test]
    fn reducer_search_noop_still_consumes_when_searching() {
        let mut is_searching = true;
        let mut query = String::from("abc");

        let out = reduce_browser_action(BrowserAction::Noop, &mut is_searching, &mut query);

        assert!(out.redraw);
        assert!(is_searching);
        assert_eq!(query, "abc");
    }

    #[test]
    fn reducer_filesystem_start_search_sets_flag_and_clears_query() {
        let mut is_searching = false;
        let mut query = String::from("abc");
        let mut state = TreeViewState::default();
        let data: Vec<RawNode<FileMetadata>> = vec![];
        let mode = FileBrowserMode::Directory;

        let out = reduce_filesystem_navigation_action(
            BrowserFsAction::StartSearch,
            &mut state,
            &data,
            &mode,
            &mut is_searching,
            &mut query,
            5,
        );

        assert!(out.consumed);
        assert!(is_searching);
        assert!(query.is_empty());
    }

    #[test]
    fn reducer_filesystem_enter_dir_emits_fetch_effect() {
        let mut is_searching = true;
        let mut query = String::from("abc");
        let mut state = TreeViewState {
            current_path: PathBuf::from("."),
            cursor_path: Some(PathBuf::from(".")),
            ..Default::default()
        };
        let data: Vec<RawNode<FileMetadata>> = vec![];
        let mode = FileBrowserMode::Directory;

        let out = reduce_filesystem_navigation_action(
            BrowserFsAction::EnterDir,
            &mut state,
            &data,
            &mode,
            &mut is_searching,
            &mut query,
            5,
        );

        assert!(out.consumed);
        assert!(!is_searching);
        assert!(query.is_empty());
        assert_eq!(out.effects.len(), 1);
        assert!(matches!(
            out.effects[0],
            BrowserFsEffect::FetchFileTree { ref path, highlight_path: None, .. }
                if path == &PathBuf::from(".")
        ));
    }

    #[test]
    fn reducer_download_edit_insert_updates_buffer_and_cursor() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            torrent_files: vec![],
            container_name: "ab".to_string(),
            use_container: true,
            is_editing_name: true,
            focused_pane: BrowserPane::TorrentPreview,
            preview_tree: vec![RawNode {
                name: "x".to_string(),
                full_path: PathBuf::from("x"),
                children: vec![],
                payload: TorrentPreviewPayload::default(),
                is_dir: false,
            }],
            preview_state: TreeViewState::default(),
            cursor_pos: 2,
            original_name_backup: "ab".to_string(),
        };

        let out = reduce_browser_download_action(
            BrowserDownloadAction::Edit(BrowserDownloadEditAction::Insert('c')),
            &mut mode,
        );
        assert!(out.consumed);
        match mode {
            FileBrowserMode::DownloadLocSelection {
                container_name,
                cursor_pos,
                ..
            } => {
                assert_eq!(container_name, "abc");
                assert_eq!(cursor_pos, 3);
            }
            _ => panic!("expected DownloadLocSelection"),
        }
    }

    #[test]
    fn reducer_download_edit_cancel_restores_backup() {
        let mut name = String::from("abc");
        let mut is_editing_name = true;
        let mut cursor_pos = 3;
        let backup = String::from("orig");

        let out = reduce_download_name_edit_action(
            BrowserDownloadEditAction::Cancel,
            &mut name,
            &mut is_editing_name,
            &mut cursor_pos,
            &backup,
        );

        assert!(out.consumed);
        assert_eq!(name, "orig");
        assert!(!is_editing_name);
        assert_eq!(cursor_pos, 4);
    }

    #[test]
    fn reducer_download_shortcut_start_rename_sets_editing_state() {
        let mut use_container = true;
        let mut is_editing_name = false;
        let mut focused_pane = BrowserPane::FileSystem;
        let mut cursor_pos = 0;
        let mut original_name_backup = String::new();
        let container_name = String::from("seed");

        let out = reduce_download_shortcut_action(
            BrowserDownloadShortcutAction::StartRename,
            &container_name,
            &mut use_container,
            &mut is_editing_name,
            &mut focused_pane,
            &mut cursor_pos,
            &mut original_name_backup,
        );

        assert!(out.consumed);
        assert!(is_editing_name);
        assert_eq!(original_name_backup, "seed");
        assert_eq!(cursor_pos, 4);
        assert_eq!(focused_pane, BrowserPane::TorrentPreview);
    }

    #[test]
    fn map_download_shortcut_requires_container_for_rename() {
        let action = map_download_shortcut_key_to_action(KeyCode::Char('r'), false);
        assert!(action.is_none());
    }

    #[test]
    fn map_download_key_prefers_edit_action_while_editing() {
        let mode = FileBrowserMode::DownloadLocSelection {
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: true,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };

        let action = map_download_key_to_action(KeyCode::Tab, &mode);

        assert!(matches!(
            action,
            Some(BrowserDownloadAction::Edit(BrowserDownloadEditAction::Noop))
        ));
    }

    #[test]
    fn reduce_browser_download_shortcut_updates_mode() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            torrent_files: vec![],
            container_name: "seed".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 4,
            original_name_backup: String::new(),
        };

        let out = reduce_browser_download_action(
            BrowserDownloadAction::Shortcut(BrowserDownloadShortcutAction::StartRename),
            &mut mode,
        );

        assert!(out.consumed);
        match mode {
            FileBrowserMode::DownloadLocSelection {
                is_editing_name,
                focused_pane,
                original_name_backup,
                ..
            } => {
                assert!(is_editing_name);
                assert_eq!(focused_pane, BrowserPane::TorrentPreview);
                assert_eq!(original_name_backup, "seed");
            }
            _ => panic!("expected DownloadLocSelection"),
        }
    }

    #[test]
    fn name_edit_guard_ignored_when_not_editing() {
        let mut mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 0,
            items: vec![],
        };
        let out = reduce_browser_download_action(
            BrowserDownloadAction::Edit(BrowserDownloadEditAction::Insert('x')),
            &mut mode,
        );
        assert!(!out.consumed);
    }

    #[test]
    fn reducer_download_shortcuts_toggle_pane() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };
        let out = reduce_browser_download_action(
            BrowserDownloadAction::Shortcut(BrowserDownloadShortcutAction::TogglePane),
            &mut mode,
        );
        assert!(out.consumed);
        match mode {
            FileBrowserMode::DownloadLocSelection { focused_pane, .. } => {
                assert_eq!(focused_pane, BrowserPane::TorrentPreview);
            }
            _ => panic!("expected DownloadLocSelection"),
        }
    }

    #[test]
    fn has_preview_content_matches_file_mode_torrent_extension() {
        let mode = FileBrowserMode::File(vec![".torrent".to_string()]);
        let path = PathBuf::from("demo.torrent");
        assert!(has_preview_content(&mode, false, false, Some(&path)));
    }

    #[test]
    fn preview_reducer_navigate_consumes_direction_key() {
        let mut tree = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![RawNode {
                name: "child".to_string(),
                full_path: PathBuf::from("root/child"),
                children: vec![],
                payload: TorrentPreviewPayload::default(),
                is_dir: false,
            }],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];
        let mut state = TreeViewState::default();
        state.expanded_paths.insert(PathBuf::from("root"));
        state.cursor_path = Some(PathBuf::from("root"));
        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Down),
            &mut state,
            &mut tree,
            Some(10),
        );
        assert!(out.consumed);
        assert_eq!(state.cursor_path, Some(PathBuf::from("root/child")));
    }

    #[test]
    fn preview_reducer_passes_through_confirm_key() {
        let mut tree: Vec<RawNode<TorrentPreviewPayload>> = vec![];
        let mut state = TreeViewState::default();
        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('Y')),
            &mut state,
            &mut tree,
            Some(10),
        );
        assert!(!out.consumed);
    }

    #[test]
    fn preview_reducer_ignores_unknown_key_with_consume() {
        let mut tree: Vec<RawNode<TorrentPreviewPayload>> = vec![];
        let mut state = TreeViewState::default();
        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('z')),
            &mut state,
            &mut tree,
            Some(10),
        );
        assert!(out.consumed);
    }

    #[test]
    fn preview_reducer_cycles_priority_on_space() {
        let mut tree = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];
        let mut state = TreeViewState {
            cursor_path: Some(PathBuf::from("root")),
            ..Default::default()
        };

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char(' ')),
            &mut state,
            &mut tree,
            Some(10),
        );

        assert!(out.consumed);
        assert_eq!(tree[0].payload.priority, FilePriority::Skip);
    }

    #[test]
    fn filesystem_navigation_starts_search() {
        let mut state = TreeViewState::default();
        let data: Vec<RawNode<FileMetadata>> = vec![];
        let mode = FileBrowserMode::Directory;
        let (tx, _rx) = mpsc::channel(1);
        let mut is_searching = false;
        let mut query = String::from("abc");
        let consumed = handle_filesystem_navigation(
            KeyCode::Char('/'),
            BrowserFilesystemNavContext {
                state: &mut state,
                data: &data,
                browser_mode: &mode,
                is_searching: &mut is_searching,
                search_query: &mut query,
                list_height: 5,
                app_command_tx: &tx,
            },
        );
        assert!(consumed);
        assert!(is_searching);
        assert!(query.is_empty());
    }

    #[test]
    fn confirm_config_path_selection_returns_config_mode() {
        let mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 2,
            items: vec![ConfigItem::WatchFolder],
        };
        let state = TreeViewState {
            current_path: PathBuf::from("/tmp"),
            ..Default::default()
        };
        let out = confirm_config_path_selection(&state, &mode);
        assert!(matches!(out, Some(ConfigUiState { .. })));
    }

    #[test]
    fn resolve_confirm_decision_prefers_config_path_mode() {
        let mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 0,
            items: vec![ConfigItem::WatchFolder],
        };
        let state = TreeViewState {
            current_path: PathBuf::from("/tmp"),
            ..Default::default()
        };
        let decision = resolve_confirm_decision(&state, &mode);
        assert!(matches!(
            decision,
            ConfirmDecision::ToConfig(ConfigUiState { .. })
        ));
    }

    #[test]
    fn reducer_dialog_confirm_emits_execute_and_clear_search() {
        let mode = FileBrowserMode::Directory;
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(
            BrowserDialogAction::ConfirmSelection,
            &state,
            &mode,
            false,
        );

        assert_eq!(out.effects.len(), 2);
        assert!(matches!(
            out.effects[0],
            BrowserDialogEffect::ExecuteConfirmDecision(_)
        ));
        assert!(matches!(out.effects[1], BrowserDialogEffect::ClearSearch));
    }

    #[test]
    fn reducer_dialog_escape_prefers_config_switch() {
        let mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 0,
            items: vec![ConfigItem::WatchFolder],
        };
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(BrowserDialogAction::Escape, &state, &mode, true);

        assert_eq!(out.effects.len(), 2);
        assert!(matches!(out.effects[0], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[1],
            BrowserDialogEffect::ToConfig(ConfigUiState { .. })
        ));
    }

    #[test]
    fn reducer_dialog_escape_directory_clears_search_and_exits_without_cleanup() {
        let mode = FileBrowserMode::Directory;
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(BrowserDialogAction::Escape, &state, &mode, true);

        assert_eq!(out.effects.len(), 2);
        assert!(matches!(out.effects[0], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[1],
            BrowserDialogEffect::ToNormalAndClearPending
        ));
    }

    #[test]
    fn reducer_dialog_escape_download_with_pending_cleans_then_exits() {
        let mode = FileBrowserMode::DownloadLocSelection {
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(BrowserDialogAction::Escape, &state, &mode, true);

        assert_eq!(out.effects.len(), 3);
        assert!(matches!(
            out.effects[0],
            BrowserDialogEffect::CleanupPendingLink {
                async_delete: false
            }
        ));
        assert!(matches!(out.effects[1], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[2],
            BrowserDialogEffect::ToNormalAndClearPending
        ));
    }

    #[test]
    fn reducer_dialog_cancel_download_emits_async_cleanup_and_exit() {
        let mode = FileBrowserMode::DownloadLocSelection {
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(
            BrowserDialogAction::CancelDownloadSelection,
            &state,
            &mode,
            true,
        );

        assert_eq!(out.effects.len(), 3);
        assert!(matches!(
            out.effects[0],
            BrowserDialogEffect::CleanupPendingLink { async_delete: true }
        ));
        assert!(matches!(out.effects[1], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[2],
            BrowserDialogEffect::ToNormalAndClearPending
        ));
    }

    #[test]
    fn pending_link_hash_is_none_for_empty() {
        assert!(pending_link_info_hash("").is_none());
    }

    #[test]
    fn cleanup_pending_link_is_noop_for_empty() {
        let mut txs: HashMap<Vec<u8>, Sender<ManagerCommand>> = HashMap::new();
        let mut torrents: HashMap<Vec<u8>, TorrentDisplayState> = HashMap::new();
        let mut order = vec![];
        cleanup_pending_link_on_escape("", &mut txs, &mut torrents, &mut order, false);
        assert!(txs.is_empty());
        assert!(torrents.is_empty());
        assert!(order.is_empty());
    }

    #[test]
    fn apply_priority_cycle_updates_target_tree() {
        let mut nodes = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![RawNode {
                name: "leaf".to_string(),
                full_path: PathBuf::from("root/leaf"),
                children: vec![],
                payload: TorrentPreviewPayload::default(),
                is_dir: false,
            }],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];

        let changed = apply_priority_cycle(&mut nodes, &PathBuf::from("root"));
        assert!(changed);
        assert_eq!(nodes[0].payload.priority, FilePriority::Skip);
        assert_eq!(nodes[0].children[0].payload.priority, FilePriority::Skip);
    }
}
