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

use std::fs;
use std::io::{ErrorKind, Stdout};

use std::collections::VecDeque;

use magnet_url::Magnet;

use fuzzy_matcher::FuzzyMatcher;

use strum_macros::EnumIter;

use crate::torrent_manager::DiskIoOperation;

use crate::config::{get_app_paths, save_settings};
use crate::config::{
    FeedSyncError, PeerSortColumn, RssFilterMode, RssHistoryEntry, Settings, SortDirection,
    TorrentSettings, TorrentSortColumn,
};
use crate::persistence::network_history::{
    load_network_history_state, save_network_history_state, NetworkHistoryPersistedState,
    NetworkHistoryRollupState,
};
use crate::persistence::rss::{load_rss_state, save_rss_state, RssPersistedState};

use crate::token_bucket::TokenBucket;

use crate::tui::effects::compute_effects_activity_speed_multiplier;
use crate::tui::events;
use crate::tui::tree;
use crate::tui::tree::RawNode;
use crate::tui::tree::TreeViewState;
use crate::tui::view::draw;

use crate::config::get_watch_path;
use crate::storage::build_fs_tree;

use crate::resource_manager::ResourceType;
use crate::telemetry::network_history_telemetry::NetworkHistoryTelemetry;
use crate::telemetry::ui_telemetry::UiTelemetry;
use crate::theme::Theme;
use crate::tuning::{make_random_adjustment, normalize_limits_for_mode, TuningController};

use crate::integrations::rss_url_safety::is_safe_rss_item_url;
use crate::integrations::status::AppOutputState;
use crate::integrations::{rss_ingest, rss_service, status, watcher};
use crate::torrent_file::parser::from_bytes;
use crate::torrent_manager::ManagerCommand;
use crate::torrent_manager::ManagerEvent;
use crate::torrent_manager::TorrentManager;
use crate::torrent_manager::TorrentParameters;

use std::collections::HashMap;
use tokio::io::AsyncReadExt;
use tokio::signal;
use tokio::sync::broadcast;
use tokio::sync::mpsc::Sender;
use tokio::sync::watch;

use std::sync::Arc;
use std::time::{Instant, SystemTime, UNIX_EPOCH};

#[cfg(feature = "dht")]
use mainline::{async_dht::AsyncDht, Dht};
#[cfg(not(feature = "dht"))]
type AsyncDht = ();

use sha1::Digest;
use sha2::Sha256;
use std::path::PathBuf;

use notify::{Error as NotifyError, Event, RecommendedWatcher, RecursiveMode, Watcher};
use serde::{Deserialize, Serialize};
use std::time::Duration;

use ratatui::prelude::Rect;
use ratatui::{backend::CrosstermBackend, Terminal};

use sysinfo::System;

use data_encoding::BASE32;

use tracing::{event as tracing_event, Level};

use crate::resource_manager::{ResourceManager, ResourceManagerClient};
use tokio::net::TcpStream;
use tokio::sync::mpsc;

use tokio::time;
use tokio::time::MissedTickBehavior;

use directories::UserDirs;

use ratatui::crossterm::event::{self, Event as CrosstermEvent};

#[cfg(unix)]
use rlimit::Resource;

const FILE_HANDLE_MINIMUM: usize = 64;
const SAFE_BUDGET_PERCENTAGE: f64 = 0.85;
pub const RSS_MAX_TORRENT_DOWNLOAD_BYTES: usize = 10 * 1024 * 1024;
const RSS_MANUAL_DOWNLOAD_TIMEOUT_SECS: u64 = 20;
const NETWORK_HISTORY_PERSIST_INTERVAL_SECS: u64 = 15 * 60;
const SHUTDOWN_TIMEOUT_SECS: u64 = 20;

#[derive(serde::Deserialize)]
struct CratesResponse {
    #[serde(rename = "crate")]
    krate: CrateInfo,
}

#[derive(serde::Deserialize)]
struct CrateInfo {
    max_version: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum FilePriority {
    #[default]
    Normal,
    High,
    Skip,
    Mixed, // Used for folders that contain children with different priorities
}

impl FilePriority {
    pub fn next(&self) -> Self {
        match self {
            Self::Normal => Self::Skip,
            Self::Skip => Self::High,
            Self::High => Self::Normal,
            Self::Mixed => Self::Normal, // Reset mixed to Normal on toggle
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct TorrentPreviewPayload {
    pub file_index: Option<usize>, // None for folders
    pub size: u64,
    pub priority: FilePriority,
}

// Implement AddAssign so RawNode::from_path_list can aggregate folder sizes
impl std::ops::AddAssign for TorrentPreviewPayload {
    fn add_assign(&mut self, rhs: Self) {
        self.size += rhs.size;
        // Logic to determine folder priority state (e.g., if children differ -> Mixed)
        if self.priority != rhs.priority {
            self.priority = FilePriority::Mixed;
        }
    }
}

#[derive(Default, Debug, Clone, PartialEq)]
pub enum BrowserPane {
    #[default]
    FileSystem,
    TorrentPreview,
}

#[derive(Default, Debug, Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum FileBrowserMode {
    #[default]
    Directory, // User must pick a folder (e.g. Download Location)
    File(Vec<String>), // User must pick a file matching these extensions (e.g. vec!["torrent"])
    // Future proofing: You could add 'AnyFile' or 'FileOrFolder' here later
    DownloadLocSelection {
        torrent_files: Vec<String>, // List of relative file paths in the torrent
        container_name: String,     // Name of the container folder (e.g. hash_name)
        use_container: bool,        // Toggle state
        is_editing_name: bool,      // Whether the user is currently typing the name
        focused_pane: BrowserPane,
        preview_tree: Vec<RawNode<TorrentPreviewPayload>>, // Interactive tree
        preview_state: TreeViewState,                      // Cursor & expansion state for preview
        cursor_pos: usize,
        original_name_backup: String,
    },
    ConfigPathSelection {
        target_item: ConfigItem,
        current_settings: Box<Settings>,
        selected_index: usize,
        items: Vec<ConfigItem>,
    },
}

#[derive(Debug, Clone)]
pub struct FileMetadata {
    pub size: u64,
    pub modified: std::time::SystemTime,
}

#[derive(Debug, Clone, Copy, Default)]
pub enum DataRate {
    RateQuarter,
    RateHalf,
    #[default]
    Rate1s,
    Rate2s,
    Rate4s,
    Rate10s,
    Rate20s,
    Rate30s,
    Rate60s,
}

impl DataRate {
    /// Returns the millisecond value for the data rate.
    pub fn as_ms(&self) -> u64 {
        match self {
            DataRate::RateQuarter => 4000,
            DataRate::RateHalf => 2000,
            DataRate::Rate1s => 1000,
            DataRate::Rate2s => 500,
            DataRate::Rate4s => 250,
            DataRate::Rate10s => 100,
            DataRate::Rate20s => 50,
            DataRate::Rate30s => 33,
            DataRate::Rate60s => 17,
        }
    }

    pub fn to_string(self) -> &'static str {
        match self {
            DataRate::RateQuarter => "0.25 FPS",
            DataRate::RateHalf => "0.5 FPS",
            DataRate::Rate1s => "1 FPS",
            DataRate::Rate2s => "2 FPS",
            DataRate::Rate4s => "4 FPS",
            DataRate::Rate10s => "10 FPS",
            DataRate::Rate20s => "20 FPS",
            DataRate::Rate30s => "30 FPS",
            DataRate::Rate60s => "60 FPS",
        }
    }

    /// Cycles to the next (slower) data rate (lower FPS).
    pub fn next_slower(&self) -> Self {
        match self {
            DataRate::Rate60s => DataRate::Rate30s,
            DataRate::Rate30s => DataRate::Rate20s,
            DataRate::Rate20s => DataRate::Rate10s,
            DataRate::Rate10s => DataRate::Rate4s,
            DataRate::Rate4s => DataRate::Rate2s,
            DataRate::Rate2s => DataRate::Rate1s,
            DataRate::Rate1s => DataRate::RateHalf,
            DataRate::RateHalf => DataRate::RateQuarter,
            DataRate::RateQuarter => DataRate::RateQuarter,
        }
    }

    /// Cycles to the previous (faster) data rate (higher FPS).
    pub fn next_faster(&self) -> Self {
        match self {
            DataRate::RateQuarter => DataRate::RateHalf,
            DataRate::RateHalf => DataRate::Rate1s,
            DataRate::Rate1s => DataRate::Rate2s,
            DataRate::Rate2s => DataRate::Rate4s,
            DataRate::Rate4s => DataRate::Rate10s,
            DataRate::Rate10s => DataRate::Rate20s,
            DataRate::Rate20s => DataRate::Rate30s,
            DataRate::Rate30s => DataRate::Rate60s,
            DataRate::Rate60s => DataRate::Rate60s,
        }
    }
}

#[derive(Default, Clone, Debug)]
pub struct CalculatedLimits {
    pub reserve_permits: usize,
    pub max_connected_peers: usize,
    pub disk_read_permits: usize,
    pub disk_write_permits: usize,
}
impl CalculatedLimits {
    pub fn into_map(self) -> HashMap<ResourceType, usize> {
        let mut map = HashMap::new();
        map.insert(ResourceType::Reserve, self.reserve_permits);
        map.insert(ResourceType::PeerConnection, self.max_connected_peers);
        map.insert(ResourceType::DiskRead, self.disk_read_permits);
        map.insert(ResourceType::DiskWrite, self.disk_write_permits);
        map
    }
}

#[derive(Default, Clone, Copy, PartialEq, Debug)]
pub enum GraphDisplayMode {
    OneMinute,
    FiveMinutes,
    #[default]
    TenMinutes,
    ThirtyMinutes,
    OneHour,
    ThreeHours,
    TwelveHours,
    TwentyFourHours,
    SevenDays,
    ThirtyDays,
    OneYear,
}

impl GraphDisplayMode {
    pub fn as_seconds(&self) -> usize {
        match self {
            Self::OneMinute => 60,
            Self::FiveMinutes => 300,
            Self::TenMinutes => 600,
            Self::ThirtyMinutes => 1800,
            Self::OneHour => 3600,
            Self::ThreeHours => 3 * 3600,
            Self::TwelveHours => 12 * 3600,
            Self::TwentyFourHours => 86_400,
            Self::SevenDays => 7 * 86_400,
            Self::ThirtyDays => 30 * 86_400,
            Self::OneYear => 365 * 86_400,
        }
    }

    pub fn to_string(self) -> &'static str {
        match self {
            Self::OneMinute => "1m",
            Self::FiveMinutes => "5m",
            Self::TenMinutes => "10m",
            Self::ThirtyMinutes => "30m",
            Self::OneHour => "1h",
            Self::ThreeHours => "3h",
            Self::TwelveHours => "12h",
            Self::TwentyFourHours => "24h",
            Self::SevenDays => "7d",
            Self::ThirtyDays => "30d",
            Self::OneYear => "1y",
        }
    }

    pub fn next(&self) -> Self {
        match self {
            Self::OneMinute => Self::FiveMinutes,
            Self::FiveMinutes => Self::TenMinutes,
            Self::TenMinutes => Self::ThirtyMinutes,
            Self::ThirtyMinutes => Self::OneHour,
            Self::OneHour => Self::ThreeHours,
            Self::ThreeHours => Self::TwelveHours,
            Self::TwelveHours => Self::TwentyFourHours,
            Self::TwentyFourHours => Self::SevenDays,
            Self::SevenDays => Self::ThirtyDays,
            Self::ThirtyDays => Self::OneYear,
            Self::OneYear => Self::OneMinute,
        }
    }

    pub fn prev(&self) -> Self {
        match self {
            Self::OneMinute => Self::OneYear,
            Self::FiveMinutes => Self::OneMinute,
            Self::TenMinutes => Self::FiveMinutes,
            Self::ThirtyMinutes => Self::TenMinutes,
            Self::OneHour => Self::ThirtyMinutes,
            Self::ThreeHours => Self::OneHour,
            Self::TwelveHours => Self::ThreeHours,
            Self::TwentyFourHours => Self::TwelveHours,
            Self::SevenDays => Self::TwentyFourHours,
            Self::ThirtyDays => Self::SevenDays,
            Self::OneYear => Self::ThirtyDays,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SelectedHeader {
    Torrent(usize),
    Peer(usize),
}
impl Default for SelectedHeader {
    fn default() -> Self {
        SelectedHeader::Torrent(0)
    }
}

pub enum AppCommand {
    AddTorrentFromFile(PathBuf),
    AddTorrentFromPathFile(PathBuf),
    AddMagnetFromFile(PathBuf),
    ClientShutdown(PathBuf),
    PortFileChanged(PathBuf),
    FetchFileTree {
        path: PathBuf,
        browser_mode: FileBrowserMode,
        highlight_path: Option<PathBuf>,
    },
    UpdateFileBrowserData {
        data: Vec<tree::RawNode<FileMetadata>>,
        highlight_path: Option<PathBuf>,
    },
    RssSyncNow,
    RssPreviewUpdated(Vec<RssPreviewItem>),
    RssSyncStatusUpdated {
        last_sync_at: Option<String>,
        next_sync_at: Option<String>,
    },
    RssFeedErrorUpdated {
        feed_url: String,
        error: Option<FeedSyncError>,
    },
    RssDownloadSelected(RssHistoryEntry),
    RssDownloadPreview(RssPreviewItem),
    NetworkHistoryLoaded(NetworkHistoryPersistedState),
    NetworkHistoryPersisted {
        request_id: u64,
        success: bool,
    },
    UpdateConfig(Settings),
    UpdateVersionAvailable(String),
}

#[derive(Clone, Copy, Debug, PartialEq, EnumIter)]
pub enum ConfigItem {
    ClientPort,
    DefaultDownloadFolder,
    WatchFolder,
    GlobalDownloadLimit,
    GlobalUploadLimit,
}

#[derive(Default)]
#[allow(clippy::large_enum_variant)]
pub enum AppMode {
    Welcome,
    #[default]
    Normal,
    Help,
    PowerSaving,
    DeleteConfirm,
    Config,
    FileBrowser,
    Rss,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum RssScreen {
    #[default]
    Unified,
    History,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum RssSectionFocus {
    Links,
    Filters,
    #[default]
    Explorer,
}

#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum TorrentControlState {
    #[default]
    Running,
    Paused,
    Deleting,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct PeerInfo {
    pub address: String,
    pub peer_id: Vec<u8>,
    pub am_choking: bool,
    pub peer_choking: bool,
    pub am_interested: bool,
    pub peer_interested: bool,
    pub bitfield: Vec<bool>,
    pub download_speed_bps: u64,
    pub upload_speed_bps: u64,
    pub total_downloaded: u64,
    pub total_uploaded: u64,
    pub last_action: String,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TorrentMetrics {
    pub torrent_control_state: TorrentControlState,
    pub info_hash: Vec<u8>,
    pub torrent_or_magnet: String,
    pub torrent_name: String,
    pub download_path: Option<PathBuf>,
    pub container_name: Option<String>,
    pub file_priorities: HashMap<usize, FilePriority>,
    pub number_of_successfully_connected_peers: usize,
    pub number_of_pieces_total: u32,
    pub number_of_pieces_completed: u32,
    pub download_speed_bps: u64,
    pub upload_speed_bps: u64,
    pub bytes_downloaded_this_tick: u64,
    pub bytes_uploaded_this_tick: u64,
    pub session_total_downloaded: u64,
    pub session_total_uploaded: u64,
    pub eta: Duration,

    #[serde(skip)]
    pub peers: Vec<PeerInfo>,
    pub activity_message: String,
    pub next_announce_in: Duration,
    pub total_size: u64,
    pub bytes_written: u64,

    #[serde(skip)]
    pub blocks_in_history: Vec<u64>,

    #[serde(skip)]
    pub blocks_out_history: Vec<u64>,

    pub blocks_in_this_tick: u64,
    pub blocks_out_this_tick: u64,
}

#[derive(Default, Debug)]
pub struct TorrentDisplayState {
    pub latest_state: TorrentMetrics,
    pub download_history: Vec<u64>,
    pub upload_history: Vec<u64>,

    pub bytes_read_this_tick: u64,
    pub bytes_written_this_tick: u64,
    pub disk_read_speed_bps: u64,
    pub disk_write_speed_bps: u64,
    pub disk_read_history_log: VecDeque<DiskIoOperation>,
    pub disk_write_history_log: VecDeque<DiskIoOperation>,
    pub disk_read_thrash_score: u64,
    pub disk_write_thrash_score: u64,

    pub smoothed_download_speed_bps: u64,
    pub smoothed_upload_speed_bps: u64,

    pub swarm_availability_history: Vec<Vec<u32>>,

    pub peers_discovered_this_tick: u64,
    pub peers_connected_this_tick: u64,
    pub peers_disconnected_this_tick: u64,
    pub peer_discovery_history: Vec<u64>,
    pub peer_connection_history: Vec<u64>,
    pub peer_disconnect_history: Vec<u64>,
    pub last_seen_session_total_downloaded: u64,
    pub last_seen_session_total_uploaded: u64,
}

#[derive(Default)]
pub struct UiState {
    pub needs_redraw: bool,
    pub effects_phase_time: f64,
    pub effects_last_wall_time: f64,
    pub effects_speed_multiplier: f64,
    pub selected_header: SelectedHeader,
    pub selected_torrent_index: usize,
    pub selected_peer_index: usize,
    pub is_searching: bool,
    pub search_query: String,
    pub config: ConfigUiState,
    pub delete_confirm: DeleteConfirmUiState,
    pub file_browser: FileBrowserUiState,
    #[allow(dead_code)]
    pub rss: RssUiState,
}

#[derive(Default)]
pub struct ConfigUiState {
    pub settings_edit: Box<Settings>,
    pub selected_index: usize,
    pub items: Vec<ConfigItem>,
    pub editing: Option<(ConfigItem, String)>,
}

#[derive(Default)]
pub struct DeleteConfirmUiState {
    pub info_hash: Vec<u8>,
    pub with_files: bool,
}

#[derive(Default)]
pub struct FileBrowserUiState {
    pub state: TreeViewState,
    pub data: Vec<RawNode<FileMetadata>>,
    pub browser_mode: FileBrowserMode,
    pub is_searching: bool,
    pub search_query: String,
}

#[derive(Default)]
#[allow(dead_code)]
pub struct RssUiState {
    pub active_screen: RssScreen,
    pub focused_section: RssSectionFocus,
    pub selected_feed_index: usize,
    pub selected_filter_index: usize,
    pub selected_explorer_index: usize,
    pub selected_history_index: usize,
    pub is_searching: bool,
    pub search_query: String,
    pub is_editing: bool,
    pub edit_buffer: String,
    pub filter_draft: String,
    pub add_feed_buffer: String,
    pub add_filter_buffer: String,
    pub add_filter_mode: RssFilterMode,
    pub delete_confirm_armed: bool,
    pub status_message: Option<String>,
    pub last_sync_request_at: Option<Instant>,
}

#[derive(Default, Clone)]
pub struct RssRuntimeState {
    pub history: Vec<RssHistoryEntry>,
    pub preview_items: Vec<RssPreviewItem>,
    pub last_sync_at: Option<String>,
    pub next_sync_at: Option<String>,
    pub feed_errors: HashMap<String, FeedSyncError>,
}

#[derive(Default, Clone)]
pub struct RssFilterRuntimeStat {
    pub downloaded_matches: usize,
    pub history_age: String,
}

#[derive(Default, Clone)]
pub struct RssDerivedState {
    pub explorer_items: Vec<RssPreviewItem>,
    pub explorer_combined_match: Vec<bool>,
    pub explorer_prioritise_matches: bool,
    pub history_hash_by_dedupe: HashMap<String, Vec<u8>>,
    pub filter_runtime_stats: HashMap<usize, RssFilterRuntimeStat>,
}

#[derive(Default, Clone)]
#[allow(dead_code)]
pub struct RssPreviewItem {
    pub dedupe_key: String,
    pub title: String,
    pub link: Option<String>,
    pub guid: Option<String>,
    pub source: Option<String>,
    pub date_iso: Option<String>,
    pub is_match: bool,
    pub is_downloaded: bool,
}

#[derive(Default)]
pub struct AppState {
    pub update_available: Option<String>,
    pub should_quit: bool,
    pub shutdown_progress: f64,
    pub system_warning: Option<String>,
    pub system_error: Option<String>,
    pub limits: CalculatedLimits,

    pub screen_area: Rect,
    pub mode: AppMode,
    pub externally_accessable_port: bool,
    pub anonymize_torrent_names: bool,

    pub pending_torrent_path: Option<PathBuf>,
    pub pending_torrent_link: String,
    pub torrents: HashMap<Vec<u8>, TorrentDisplayState>,

    pub torrent_list_order: Vec<Vec<u8>>,

    pub total_download_history: Vec<u64>,
    pub total_upload_history: Vec<u64>,
    pub avg_download_history: Vec<u64>,
    pub avg_upload_history: Vec<u64>,
    pub disk_backoff_history_ms: VecDeque<u64>,
    pub minute_disk_backoff_history_ms: VecDeque<u64>,
    pub max_disk_backoff_this_tick_ms: u64,

    pub lifetime_downloaded_from_config: u64,
    pub lifetime_uploaded_from_config: u64,

    pub session_total_downloaded: u64,
    pub session_total_uploaded: u64,

    pub cpu_usage: f32,
    pub ram_usage_percent: f32,
    pub avg_disk_read_bps: u64,
    pub avg_disk_write_bps: u64,

    pub disk_read_history: Vec<u64>,
    pub disk_write_history: Vec<u64>,
    pub app_ram_usage: u64,

    pub run_time: u64,

    pub global_disk_read_history_log: VecDeque<DiskIoOperation>,
    pub global_disk_write_history_log: VecDeque<DiskIoOperation>,
    pub global_disk_read_thrash_score: u64,
    pub global_disk_write_thrash_score: u64,

    pub read_op_start_times: VecDeque<Instant>,
    pub write_op_start_times: VecDeque<Instant>,
    pub read_latency_ema: f64,
    pub write_latency_ema: f64,
    pub avg_disk_read_latency: Duration,
    pub avg_disk_write_latency: Duration,
    pub reads_completed_this_tick: u32,
    pub writes_completed_this_tick: u32,
    pub read_iops: u32,
    pub write_iops: u32,

    pub ui: UiState,
    pub rss_runtime: RssRuntimeState,
    pub rss_derived: RssDerivedState,
    pub data_rate: DataRate,
    pub theme: Theme,

    pub torrent_sort: (TorrentSortColumn, SortDirection),
    pub peer_sort: (PeerSortColumn, SortDirection),

    pub graph_mode: GraphDisplayMode,
    pub minute_avg_dl_history: Vec<u64>,
    pub minute_avg_ul_history: Vec<u64>,
    pub network_history_state: NetworkHistoryPersistedState,
    pub network_history_rollups: NetworkHistoryRollupState,
    pub network_history_dirty: bool,
    pub network_history_restore_pending: bool,
    pub next_network_history_persist_request_id: u64,
    pub pending_network_history_persist_request_id: Option<u64>,

    pub last_tuning_score: u64,
    pub current_tuning_score: u64,
    pub tuning_countdown: u64,
    pub last_tuning_limits: CalculatedLimits,
    pub is_seeding: bool,
    pub baseline_speed_ema: f64,
    pub global_disk_thrash_score: f64,
    pub adaptive_max_scpb: f64,
    pub global_seek_cost_per_byte_history: Vec<f64>,
    pub disk_health_ema: f64,
    pub disk_health_phase: f64,
    pub disk_health_peak_hold: f64,
    pub disk_health_state_level: u8,

    pub recently_processed_files: HashMap<PathBuf, Instant>,
}

pub struct App {
    pub app_state: AppState,
    pub client_configs: Settings,
    pub base_system_warning: Option<String>,
    #[cfg(feature = "dht")]
    pub dht_bootstrap_warning: Option<String>,

    pub listener: tokio::net::TcpListener,

    pub torrent_manager_incoming_peer_txs: HashMap<Vec<u8>, Sender<(TcpStream, Vec<u8>)>>,
    pub torrent_manager_command_txs: HashMap<Vec<u8>, Sender<ManagerCommand>>,
    pub distributed_hash_table: AsyncDht,
    pub resource_manager: ResourceManagerClient,
    pub global_dl_bucket: Arc<TokenBucket>,
    pub global_ul_bucket: Arc<TokenBucket>,

    pub torrent_metric_watch_rxs: HashMap<Vec<u8>, watch::Receiver<TorrentMetrics>>,
    pub manager_event_tx: mpsc::Sender<ManagerEvent>,
    pub manager_event_rx: mpsc::Receiver<ManagerEvent>,
    pub app_command_tx: mpsc::Sender<AppCommand>,
    pub app_command_rx: mpsc::Receiver<AppCommand>,
    pub rss_sync_tx: mpsc::Sender<()>,
    pub rss_downloaded_entry_tx: mpsc::Sender<RssHistoryEntry>,
    pub rss_settings_tx: watch::Sender<Settings>,
    pub tui_event_tx: mpsc::Sender<CrosstermEvent>,
    pub tui_event_rx: mpsc::Receiver<CrosstermEvent>,
    pub shutdown_tx: broadcast::Sender<()>,
    pub persistence_tx: Option<watch::Sender<Option<PersistPayload>>>,
    pub persistence_task: Option<tokio::task::JoinHandle<()>>,
    pub tui_task: Option<tokio::task::JoinHandle<()>>,
    pub notify_rx: mpsc::Receiver<Result<Event, NotifyError>>,
    pub watcher: RecommendedWatcher,
    pub tuning_controller: TuningController,
    pub next_tuning_at: time::Instant,
}

#[derive(Clone)]
pub struct NetworkHistoryPersistRequest {
    pub request_id: u64,
    pub state: NetworkHistoryPersistedState,
}

#[derive(Clone)]
pub struct PersistPayload {
    pub settings: Settings,
    pub rss_state: RssPersistedState,
    pub network_history: Option<NetworkHistoryPersistRequest>,
}
impl App {
    pub async fn new(client_configs: Settings) -> Result<Self, Box<dyn std::error::Error>> {
        let listener =
            tokio::net::TcpListener::bind(format!("0.0.0.0:{}", client_configs.client_port))
                .await?;

        let (manager_event_tx, manager_event_rx) = mpsc::channel::<ManagerEvent>(1000);
        let (app_command_tx, app_command_rx) = mpsc::channel::<AppCommand>(10);
        let (rss_sync_tx, rss_sync_rx) = mpsc::channel::<()>(8);
        let (rss_downloaded_entry_tx, rss_downloaded_entry_rx) =
            mpsc::channel::<RssHistoryEntry>(64);
        let (rss_settings_tx, rss_settings_rx) = watch::channel(client_configs.clone());
        let (tui_event_tx, tui_event_rx) = mpsc::channel::<CrosstermEvent>(100);
        let (shutdown_tx, _) = broadcast::channel(1);
        let (persistence_tx, mut persistence_rx) = watch::channel::<Option<PersistPayload>>(None);
        let persistence_app_command_tx = app_command_tx.clone();
        let persistence_task = tokio::spawn(async move {
            while persistence_rx.changed().await.is_ok() {
                let Some(payload) = persistence_rx.borrow().clone() else {
                    continue;
                };
                let network_history_request_id = payload
                    .network_history
                    .as_ref()
                    .map(|request| request.request_id);
                let write_result = tokio::task::spawn_blocking(move || {
                    save_settings(&payload.settings)
                        .map_err(|e| format!("Failed to auto-save settings: {}", e))?;
                    save_rss_state(&payload.rss_state)
                        .map_err(|e| format!("Failed to auto-save RSS state: {}", e))?;
                    if let Some(network_history) = payload.network_history {
                        save_network_history_state(&network_history.state).map_err(|e| {
                            format!("Failed to auto-save network history state: {}", e)
                        })?;
                    }
                    Ok::<(), String>(())
                })
                .await;

                match write_result {
                    Ok(Ok(())) => {
                        tracing_event!(
                            Level::DEBUG,
                            "Persistence payload auto-saved successfully."
                        );
                        if let Some(request_id) = network_history_request_id {
                            let _ = persistence_app_command_tx
                                .send(AppCommand::NetworkHistoryPersisted {
                                    request_id,
                                    success: true,
                                })
                                .await;
                        }
                    }
                    Ok(Err(e)) => {
                        tracing_event!(Level::ERROR, "{}", e);
                        if let Some(request_id) = network_history_request_id {
                            let _ = persistence_app_command_tx
                                .send(AppCommand::NetworkHistoryPersisted {
                                    request_id,
                                    success: false,
                                })
                                .await;
                        }
                    }
                    Err(e) => {
                        tracing_event!(Level::ERROR, "Persistence writer join failed: {}", e);
                        if let Some(request_id) = network_history_request_id {
                            let _ = persistence_app_command_tx
                                .send(AppCommand::NetworkHistoryPersisted {
                                    request_id,
                                    success: false,
                                })
                                .await;
                        }
                    }
                }
            }
        });

        let (limits, system_warning) = calculate_adaptive_limits(&client_configs);
        tracing_event!(
            Level::DEBUG,
            "Adaptive limits calculated: max_peers={}, disk_reads={}, disk_writes={}",
            limits.max_connected_peers,
            limits.disk_read_permits,
            limits.disk_write_permits
        );
        let mut rm_limits = HashMap::new();
        rm_limits.insert(ResourceType::Reserve, (limits.reserve_permits, 0));
        rm_limits.insert(
            ResourceType::PeerConnection,
            (limits.max_connected_peers, limits.max_connected_peers * 2),
        );
        rm_limits.insert(
            ResourceType::DiskRead,
            (limits.disk_read_permits, limits.disk_read_permits * 2),
        );
        rm_limits.insert(
            ResourceType::DiskWrite,
            (limits.disk_write_permits, limits.disk_read_permits * 2),
        );
        let (resource_manager, resource_manager_client) =
            ResourceManager::new(rm_limits, shutdown_tx.clone());
        tokio::spawn(resource_manager.run());

        #[cfg(feature = "dht")]
        let bootstrap_nodes: Vec<&str> = client_configs
            .bootstrap_nodes
            .iter()
            .map(AsRef::as_ref)
            .collect();

        #[cfg(feature = "dht")]
        let (distributed_hash_table, dht_bootstrap_warning) = match Dht::builder()
            .bootstrap(&bootstrap_nodes)
            .port(client_configs.client_port)
            .server_mode()
            .build()
        {
            Ok(dht_server) => (dht_server.as_async(), None),
            Err(e) => {
                let warning = format!(
                    "Warning: DHT bootstrap unavailable ({}). Running without bootstrap; retrying automatically.",
                    e
                );
                tracing_event!(Level::WARN, "{}", warning);
                let fallback = Dht::builder()
                    .port(client_configs.client_port)
                    .server_mode()
                    .build()
                    .map_err(|fallback_err| {
                        format!(
                            "Failed to initialize DHT startup fallback. Bootstrap error: {}. Fallback error: {}",
                            e, fallback_err
                        )
                    })?
                    .as_async();
                (fallback, Some(warning))
            }
        };

        #[cfg(not(feature = "dht"))]
        let distributed_hash_table = ();

        let dl_limit = client_configs.global_download_limit_bps as f64;
        let ul_limit = client_configs.global_upload_limit_bps as f64;
        let global_dl_bucket = Arc::new(TokenBucket::new(dl_limit, dl_limit));
        let global_ul_bucket = Arc::new(TokenBucket::new(ul_limit, ul_limit));
        let persisted_rss_state = load_rss_state();

        let tuning_controller = TuningController::new_adaptive(limits.clone());
        let tuning_state = tuning_controller.state().clone();
        let app_state = AppState {
            system_warning: None,
            system_error: None,
            limits: limits.clone(),
            ui: UiState {
                needs_redraw: true,
                ..Default::default()
            },
            theme: Theme::builtin(client_configs.ui_theme),
            torrent_sort: (
                client_configs.torrent_sort_column,
                client_configs.torrent_sort_direction,
            ),
            peer_sort: (
                client_configs.peer_sort_column,
                client_configs.peer_sort_direction,
            ),
            rss_runtime: RssRuntimeState {
                history: persisted_rss_state.history,
                preview_items: Vec::new(),
                last_sync_at: persisted_rss_state.last_sync_at,
                next_sync_at: None,
                feed_errors: persisted_rss_state.feed_errors,
            },
            lifetime_downloaded_from_config: client_configs.lifetime_downloaded,
            lifetime_uploaded_from_config: client_configs.lifetime_uploaded,
            minute_disk_backoff_history_ms: VecDeque::with_capacity(24 * 60),
            max_disk_backoff_this_tick_ms: 0,
            last_tuning_score: tuning_state.last_tuning_score,
            current_tuning_score: tuning_state.current_tuning_score,
            tuning_countdown: tuning_controller.cadence_secs(),
            last_tuning_limits: tuning_state.last_tuning_limits,
            baseline_speed_ema: tuning_state.baseline_speed_ema,
            adaptive_max_scpb: 10.0,
            ..Default::default()
        };

        let (notify_tx, notify_rx) = mpsc::channel::<Result<Event, NotifyError>>(100);
        let watcher = watcher::create_watcher(&client_configs, notify_tx)?;
        let initial_tuning_deadline =
            time::Instant::now() + Duration::from_secs(tuning_controller.cadence_secs());

        let mut app = Self {
            app_state,
            client_configs: client_configs.clone(),
            base_system_warning: system_warning,
            #[cfg(feature = "dht")]
            dht_bootstrap_warning,
            listener,
            torrent_manager_incoming_peer_txs: HashMap::new(),
            torrent_manager_command_txs: HashMap::new(),
            distributed_hash_table,
            resource_manager: resource_manager_client,
            global_dl_bucket,
            global_ul_bucket,
            torrent_metric_watch_rxs: HashMap::new(),
            manager_event_tx,
            manager_event_rx,
            app_command_tx,
            app_command_rx,
            rss_sync_tx,
            rss_downloaded_entry_tx,
            rss_settings_tx,
            tui_event_tx,
            tui_event_rx,
            shutdown_tx,
            persistence_tx: Some(persistence_tx),
            persistence_task: Some(persistence_task),
            tui_task: None,
            watcher,
            notify_rx,
            tuning_controller,
            next_tuning_at: initial_tuning_deadline,
        };
        app.refresh_system_warning();

        let _rss_service_task = rss_service::spawn_rss_service(
            app.client_configs.clone(),
            app.app_state.rss_runtime.history.clone(),
            app.app_command_tx.clone(),
            rss_sync_rx,
            rss_downloaded_entry_rx,
            rss_settings_rx,
            app.shutdown_tx.clone(),
        );

        let mut torrents_to_load = app.client_configs.torrents.clone();
        torrents_to_load.sort_by_key(|t| !t.validation_status);
        for torrent_config in torrents_to_load {
            if !should_load_persisted_torrent(&torrent_config) {
                tracing_event!(
                    Level::WARN,
                    torrent = %torrent_config.torrent_or_magnet,
                    "Skipping persisted torrent left in transient Deleting state during startup"
                );
                continue;
            }
            if torrent_config.torrent_or_magnet.starts_with("magnet:") {
                app.add_magnet_torrent(
                    torrent_config.name.clone(),
                    torrent_config.torrent_or_magnet.clone(),
                    torrent_config.download_path.clone(),
                    torrent_config.validation_status,
                    torrent_config.torrent_control_state,
                    torrent_config.file_priorities,
                    torrent_config.container_name,
                )
                .await;
            } else {
                app.add_torrent_from_file(
                    PathBuf::from(&torrent_config.torrent_or_magnet),
                    torrent_config.download_path.clone(),
                    torrent_config.validation_status,
                    torrent_config.torrent_control_state,
                    torrent_config.file_priorities.clone(),
                    torrent_config.container_name,
                )
                .await;
            }
        }

        if app.app_state.torrents.is_empty() && app.app_state.lifetime_downloaded_from_config == 0 {
            app.app_state.mode = AppMode::Welcome;
        }

        let is_leeching = app.app_state.torrents.values().any(|t| {
            t.latest_state.number_of_pieces_completed < t.latest_state.number_of_pieces_total
        });
        app.app_state.is_seeding = !is_leeching;
        app.refresh_rss_derived();

        Ok(app)
    }

    pub async fn run(
        &mut self,
        terminal: &mut Terminal<CrosstermBackend<Stdout>>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if let Ok(size) = terminal.size() {
            self.app_state.screen_area = Rect::new(0, 0, size.width, size.height);
        }

        self.process_pending_commands().await;

        self.startup_crossterm_event_listener();
        self.startup_network_history_restore();

        let mut sys = System::new();

        let mut stats_interval = time::interval(Duration::from_secs(1));
        let mut version_interval = time::interval(Duration::from_secs(24 * 60 * 60));
        let mut dht_bootstrap_retry_interval = time::interval(Duration::from_secs(60));
        let mut network_history_persist_interval =
            time::interval(Duration::from_secs(NETWORK_HISTORY_PERSIST_INTERVAL_SECS));
        self.reschedule_tuning_deadline();
        dht_bootstrap_retry_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
        network_history_persist_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);

        let output_status_interval = self.client_configs.output_status_interval;
        let mut status_dump_timer = tokio::time::interval(std::time::Duration::from_secs(
            output_status_interval.max(1),
        ));

        self.save_state_to_disk();
        self.dump_status_to_file();

        let mut next_draw_time = Instant::now();
        while !self.app_state.should_quit {
            let current_target_framerate = match self.app_state.mode {
                AppMode::Welcome => Duration::from_millis(16), // Force 60 FPS for animation
                AppMode::PowerSaving => Duration::from_secs(1), // Force 1 FPS for Zen mode
                _ => Duration::from_millis(self.app_state.data_rate.as_ms()), // User-defined FPS
            };
            let next_tuning_at = self.next_tuning_at;

            tokio::select! {
                _ = signal::ctrl_c() => {
                    self.app_state.should_quit = true;
                }
                Ok(Ok((stream, _addr))) = tokio::time::timeout(Duration::from_secs(2), self.listener.accept()) => {
                    self.handle_incoming_peer(stream).await;

                }
                Some(event) = self.manager_event_rx.recv() => {
                    self.handle_manager_event(event);
                    self.app_state.ui.needs_redraw = true;
                }

                Some(command) = self.app_command_rx.recv() => {
                    self.handle_app_command(command).await;
                },

                Some(event) = self.tui_event_rx.recv() => {
                    self.clamp_selected_indices();
                    events::handle_event(event, self).await;
                    next_draw_time = Instant::now();
                }

                Some(result) = self.notify_rx.recv() => {
                    self.handle_file_event(result).await;
                }

                _ = stats_interval.tick() => {
                    self.calculate_stats(&mut sys);
                    self.app_state.ui.needs_redraw = true;
                }

                _ = time::sleep_until(next_tuning_at) => {
                    self.tuning_resource_limits().await;
                    self.reschedule_tuning_deadline();
                }

                _ = status_dump_timer.tick(), if output_status_interval > 0 => {
                    self.dump_status_to_file();
                }
                _ = network_history_persist_interval.tick() => {
                    if should_persist_network_history_on_interval(&self.app_state) {
                        self.save_state_to_disk();
                    }
                }

                _ = time::sleep_until(next_draw_time.into()) => {
                    next_draw_time = Instant::now() + current_target_framerate;
                    self.drain_latest_torrent_metrics();
                    if Self::should_draw_this_frame(&self.app_state.mode, self.app_state.ui.needs_redraw) {
                        self.tick_ui_effects_clock();
                        terminal.draw(|f| {
                            draw(f, &self.app_state, &self.client_configs);
                        })?;
                        self.app_state.ui.needs_redraw = false;
                    }
                }
                _ = version_interval.tick() => {
                    let current_version = env!("CARGO_PKG_VERSION");
                    let tx = self.app_command_tx.clone();
                    let mut shutdown_rx = self.shutdown_tx.subscribe();

                    tokio::spawn(async move {
                        tokio::select! {
                            latest_result = App::fetch_latest_version() => {
                                if let Ok(latest) = latest_result {
                                    if latest != current_version {
                                        tracing::info!("New version found! Current: {} - Latest: {}", current_version, latest.clone());
                                        let _ = tx.send(AppCommand::UpdateVersionAvailable(latest)).await;
                                    }
                                    else {
                                        tracing::info!("Current version is latest! Current: {} - Latest: {}", current_version, latest);
                                    }
                                }
                            }
                            _ = shutdown_rx.recv() => {
                                tracing::debug!("Version check aborted due to shutdown");
                            }
                        }
                    });
                }
                _ = dht_bootstrap_retry_interval.tick() => {
                    if self.should_retry_dht_bootstrap() {
                        self.maybe_retry_dht_bootstrap();
                    }
                }
            }
        }

        self.save_state_to_disk();

        self.shutdown_sequence(terminal).await;
        self.flush_persistence_writer().await;

        Ok(())
    }

    fn should_draw_this_frame(mode: &AppMode, ui_needs_redraw: bool) -> bool {
        if matches!(mode, AppMode::PowerSaving) {
            ui_needs_redraw
        } else {
            true
        }
    }

    fn tick_ui_effects_clock(&mut self) {
        let frame_wall_time = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs_f64();
        let activity_speed_multiplier =
            compute_effects_activity_speed_multiplier(&self.app_state, &self.client_configs);

        if self.app_state.ui.effects_last_wall_time <= 0.0 {
            self.app_state.ui.effects_last_wall_time = frame_wall_time;
        }

        let frame_dt =
            (frame_wall_time - self.app_state.ui.effects_last_wall_time).clamp(0.0, 0.25);
        self.app_state.ui.effects_last_wall_time = frame_wall_time;
        self.app_state.ui.effects_speed_multiplier = activity_speed_multiplier;
        self.app_state.ui.effects_phase_time += frame_dt * activity_speed_multiplier;

        let disk_activity = self
            .app_state
            .disk_health_ema
            .max(self.app_state.disk_health_peak_hold)
            .clamp(0.0, 1.0);
        let disk_phase_speed = 1.6 + 5.0 * disk_activity;
        self.app_state.disk_health_phase = (self.app_state.disk_health_phase
            + frame_dt * disk_phase_speed)
            .rem_euclid(std::f64::consts::TAU);
    }

    fn refresh_system_warning(&mut self) {
        self.app_state.system_warning =
            compose_system_warning(self.base_system_warning.as_deref(), {
                #[cfg(feature = "dht")]
                {
                    self.dht_bootstrap_warning.as_deref()
                }
                #[cfg(not(feature = "dht"))]
                {
                    None
                }
            });
    }

    #[cfg(feature = "dht")]
    fn should_retry_dht_bootstrap(&self) -> bool {
        self.dht_bootstrap_warning.is_some()
    }

    #[cfg(not(feature = "dht"))]
    fn should_retry_dht_bootstrap(&self) -> bool {
        false
    }

    #[cfg(feature = "dht")]
    fn maybe_retry_dht_bootstrap(&mut self) {
        self.retry_dht_bootstrap();
    }

    #[cfg(not(feature = "dht"))]
    fn maybe_retry_dht_bootstrap(&mut self) {}

    #[cfg(feature = "dht")]
    fn retry_dht_bootstrap(&mut self) {
        let bootstrap_nodes: Vec<&str> = self
            .client_configs
            .bootstrap_nodes
            .iter()
            .map(AsRef::as_ref)
            .collect();

        match Dht::builder()
            .bootstrap(&bootstrap_nodes)
            .port(self.client_configs.client_port)
            .server_mode()
            .build()
        {
            Ok(new_dht_server) => {
                let new_dht_handle = new_dht_server.as_async();
                self.distributed_hash_table = new_dht_handle.clone();
                for manager_tx in self.torrent_manager_command_txs.values() {
                    let _ = manager_tx
                        .try_send(ManagerCommand::UpdateDhtHandle(new_dht_handle.clone()));
                }
                self.dht_bootstrap_warning = None;
                self.refresh_system_warning();
                tracing_event!(Level::INFO, "DHT bootstrap recovered.");
            }
            Err(e) => {
                tracing_event!(Level::DEBUG, "DHT bootstrap retry failed: {}", e);
            }
        }
    }

    fn startup_crossterm_event_listener(&mut self) {
        let tui_event_tx_clone = self.tui_event_tx.clone();
        let mut tui_shutdown_rx = self.shutdown_tx.subscribe();

        self.tui_task = Some(tokio::spawn(async move {
            loop {
                if tui_shutdown_rx.try_recv().is_ok() {
                    break;
                }

                // Run blocking poll to completion (do NOT use tokio::select!)
                // This ensures we never abandon a thread that is reading from stdin.
                // Keep the timeout relatively short (250ms) so the app remains responsive to shutdown.
                let event =
                    tokio::task::spawn_blocking(|| -> std::io::Result<Option<CrosstermEvent>> {
                        if event::poll(Duration::from_millis(250))? {
                            return Ok(Some(event::read()?));
                        }
                        Ok(None)
                    })
                    .await;

                match event {
                    Ok(Ok(Some(e))) => {
                        if tui_event_tx_clone.send(e).await.is_err() {
                            break;
                        }
                    }
                    Ok(Ok(None)) => {}
                    Ok(Err(e)) => {
                        tracing::error!("Crossterm event error: {}", e);
                        break;
                    }
                    Err(e) => {
                        tracing::error!("Blocking task join error: {}", e);
                        break;
                    }
                }

                if tui_shutdown_rx.try_recv().is_ok() {
                    break;
                }
            }
        }));
    }

    async fn flush_persistence_writer(&mut self) {
        flush_persistence_writer_parts(&mut self.persistence_tx, &mut self.persistence_task).await;
    }

    async fn shutdown_sequence(&mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) {
        let _ = self.shutdown_tx.send(());

        if let Some(handle) = self.tui_task.take() {
            tracing::info!("Waiting for TUI event listener to finish...");
            if let Err(e) = handle.await {
                tracing::error!("Error joining TUI task: {}", e);
            }
        }

        let total_managers_to_shut_down = self.torrent_manager_command_txs.len();
        let mut managers_shut_down = 0;

        for manager_tx in self.torrent_manager_command_txs.values() {
            let _ = manager_tx.try_send(ManagerCommand::Shutdown);
        }

        if total_managers_to_shut_down == 0 {
            return;
        }

        let shutdown_timeout = time::sleep(Duration::from_secs(SHUTDOWN_TIMEOUT_SECS));
        let mut draw_interval = time::interval(Duration::from_millis(100));
        tokio::pin!(shutdown_timeout);

        tracing_event!(
            Level::INFO,
            "Waiting for {} torrents to shut down...",
            total_managers_to_shut_down
        );

        loop {
            self.app_state.shutdown_progress =
                managers_shut_down as f64 / total_managers_to_shut_down as f64;
            self.tick_ui_effects_clock();
            let _ = terminal.draw(|f| {
                draw(f, &self.app_state, &self.client_configs);
            });

            tokio::select! {
                Some(event) = self.manager_event_rx.recv() => {
                    match event {
                        ManagerEvent::DeletionComplete(..) => {
                            managers_shut_down += 1;
                            if managers_shut_down == total_managers_to_shut_down {
                                tracing_event!(Level::INFO, "All torrents shut down gracefully.");
                                break;
                            }
                        }
                        _ => {
                            // CRITICAL: We must aggressively drain other events (Stats, BlockReceived, etc.)
                            // so the managers don't get blocked on a full channel while trying to die.
                        }
                    }
                }

                _ = draw_interval.tick() => {
                }

                _ = &mut shutdown_timeout => {
                    tracing_event!(Level::WARN, "Shutdown timed out. {}/{} managers did not reply. Forcing exit.",
                        total_managers_to_shut_down - managers_shut_down,
                        total_managers_to_shut_down
                    );
                    break;
                }
            }
        }
    }

    async fn handle_incoming_peer(&mut self, mut stream: TcpStream) {
        if !self.app_state.externally_accessable_port {
            self.app_state.externally_accessable_port = true;
        }

        let torrent_manager_incoming_peer_txs_clone =
            self.torrent_manager_incoming_peer_txs.clone();
        let resource_manager_clone = self.resource_manager.clone();
        let mut permit_shutdown_rx = self.shutdown_tx.subscribe();
        tokio::spawn(async move {
            let _session_permit = tokio::select! {
                permit_result = resource_manager_clone.acquire_peer_connection() => {
                    match permit_result {
                        Ok(permit) => Some(permit),
                        Err(_) => {
                            tracing_event!(Level::DEBUG, "Failed to acquire permit. Manager shut down?");
                            None
                        }
                    }
                }
                _ = permit_shutdown_rx.recv() => {
                    None
                }
            };
            let mut buffer = vec![0u8; 68];
            if (stream.read_exact(&mut buffer).await).is_ok() {
                let peer_info_hash = &buffer[28..48];

                if let Some(torrent_manager_tx) =
                    torrent_manager_incoming_peer_txs_clone.get(peer_info_hash)
                {
                    let torrent_manager_tx_clone = torrent_manager_tx.clone();
                    let _ = torrent_manager_tx_clone.send((stream, buffer)).await;
                } else {
                    tracing::trace!(
                        "ROUTING FAIL: No manager registered for hash: {}",
                        hex::encode(peer_info_hash)
                    );
                }
            }
        });
    }

    fn refresh_rss_derived(&mut self) {
        crate::tui::screens::rss::recompute_rss_derived(&mut self.app_state, &self.client_configs);
    }

    async fn handle_app_command(&mut self, command: AppCommand) {
        match command {
            AppCommand::AddTorrentFromFile(path) => {
                // Determine if the file is coming from a watch folder (User or System)
                let parent_dir = path.parent();
                let is_user_watch = self
                    .client_configs
                    .watch_folder
                    .as_ref()
                    .is_some_and(|p| parent_dir == Some(p));

                let system_watch_info = get_watch_path();
                let is_system_watch = system_watch_info
                    .as_ref()
                    .is_some_and(|(p, _)| parent_dir == Some(p));

                if let Some(download_path) = &self.client_configs.default_download_folder {
                    // --- CASE A: Automatic Adding (Default Path Exists) ---
                    self.add_torrent_from_file(
                        path.to_path_buf(),
                        Some(download_path.to_path_buf()),
                        false,
                        TorrentControlState::Running,
                        HashMap::new(),
                        None,
                    )
                    .await;

                    self.save_state_to_disk();

                    // Cleanup: Move to processed or rename to .added
                    if is_user_watch || is_system_watch {
                        let move_successful = if let Some((_, processed_path)) = system_watch_info {
                            (|| {
                                fs::create_dir_all(&processed_path).ok()?;
                                let file_name = path.file_name()?;
                                let new_path = processed_path.join(file_name);
                                fs::rename(&path, &new_path).ok()?;
                                Some(())
                            })()
                            .is_some()
                        } else {
                            false
                        };

                        if !move_successful {
                            let mut new_path = path.clone();
                            new_path.set_extension("torrent.added");
                            let _ = fs::rename(&path, &new_path);
                        }
                    }
                } else {
                    // --- CASE B: Manual Adding (Prompt for Location) ---
                    if let Ok(buffer) = fs::read(&path) {
                        if let Ok(torrent) = from_bytes(&buffer) {
                            // 1. Rename the file immediately if it's in a watch folder
                            // This prevents the watcher from re-triggering while the UI is open.
                            let final_path = if is_user_watch || is_system_watch {
                                let mut new_path = path.clone();
                                new_path.set_extension("torrent.added");
                                if let Err(e) = fs::rename(&path, &new_path) {
                                    tracing::error!("Failed to rename watched file: {}", e);
                                    path.clone()
                                } else {
                                    new_path
                                }
                            } else {
                                path.clone()
                            };

                            // 2. Parse metadata for the UI Preview
                            let info_hash = if torrent.info.meta_version == Some(2) {
                                let mut hasher = Sha256::new();
                                hasher.update(&torrent.info_dict_bencode);
                                hasher.finalize()[0..20].to_vec()
                            } else {
                                let mut hasher = sha1::Sha1::new();
                                hasher.update(&torrent.info_dict_bencode);
                                hasher.finalize().to_vec()
                            };

                            let info_hash_hex = hex::encode(&info_hash);
                            let default_container_name =
                                format!("{} [{}]", torrent.info.name, info_hash_hex);
                            let file_list = torrent.file_list();
                            let should_enclose = file_list.len() > 1;

                            // 3. Build Preview Tree
                            let preview_payloads: Vec<(Vec<String>, TorrentPreviewPayload)> =
                                file_list
                                    .into_iter()
                                    .enumerate()
                                    .map(|(idx, (parts, size))| {
                                        (
                                            parts,
                                            TorrentPreviewPayload {
                                                file_index: Some(idx),
                                                size,
                                                priority: FilePriority::Normal,
                                            },
                                        )
                                    })
                                    .collect();

                            let preview_tree = RawNode::from_path_list(None, preview_payloads);
                            let mut preview_state = TreeViewState::new();
                            for node in &preview_tree {
                                node.expand_all(&mut preview_state);
                            }

                            // 4. Update state and switch to File Browser
                            self.app_state.pending_torrent_path = Some(final_path);
                            let initial_path = self.get_initial_destination_path();

                            let _ = self.app_command_tx.try_send(AppCommand::FetchFileTree {
                                path: initial_path,
                                browser_mode: FileBrowserMode::DownloadLocSelection {
                                    torrent_files: vec![],
                                    container_name: default_container_name.clone(),
                                    use_container: should_enclose,
                                    is_editing_name: false,
                                    preview_tree,
                                    preview_state,
                                    focused_pane: BrowserPane::FileSystem,
                                    cursor_pos: 0,
                                    original_name_backup: default_container_name,
                                },
                                highlight_path: None,
                            });
                        } else {
                            self.app_state.system_error =
                                Some("Failed to parse torrent file for preview.".to_string());
                        }
                    } else {
                        self.app_state.system_error =
                            Some("Failed to read torrent file.".to_string());
                    }
                }
            }
            AppCommand::AddTorrentFromPathFile(path) => {
                if let Some((_, processed_path)) = get_watch_path() {
                    match fs::read_to_string(&path) {
                        Ok(torrent_file_path_str) => {
                            let torrent_file_path = PathBuf::from(torrent_file_path_str.trim());
                            if let Some(download_path) =
                                self.client_configs.default_download_folder.clone()
                            {
                                self.add_torrent_from_file(
                                    torrent_file_path,
                                    Some(download_path),
                                    false,
                                    TorrentControlState::Running,
                                    HashMap::new(),
                                    None,
                                )
                                .await;
                                self.save_state_to_disk();
                            } else {
                                self.app_state.pending_torrent_path = Some(torrent_file_path);
                                let initial_path = self.get_initial_destination_path();

                                let _ = self.app_command_tx.try_send(AppCommand::FetchFileTree {
                                    path: initial_path,
                                    browser_mode: FileBrowserMode::DownloadLocSelection {
                                        torrent_files: vec![],
                                        container_name: "New Torrent".to_string(),
                                        use_container: true,
                                        is_editing_name: false,
                                        preview_tree: Vec::new(),
                                        preview_state: TreeViewState::default(),
                                        focused_pane: BrowserPane::FileSystem,
                                        cursor_pos: 0,
                                        original_name_backup: "New Torrent".to_string(),
                                    },
                                    highlight_path: None,
                                });
                            }
                        }
                        Err(e) => {
                            tracing_event!(
                                Level::ERROR,
                                "Failed to read torrent path from file {:?}: {}",
                                &path,
                                e
                            );
                        }
                    }

                    if let Some(file_name) = path.file_name() {
                        let new_path = processed_path.join(file_name);
                        if let Err(e) = fs::rename(&path, &new_path) {
                            tracing_event!(
                                Level::WARN,
                                "Failed to move processed path file {:?}: {}",
                                &path,
                                e
                            );
                        }
                    }
                }
            }
            AppCommand::AddMagnetFromFile(path) => {
                if let Some((_, processed_path)) = get_watch_path() {
                    match fs::read_to_string(&path) {
                        Ok(magnet_link) => {
                            if let Some(download_path) =
                                self.client_configs.default_download_folder.clone()
                            {
                                self.add_magnet_torrent(
                                    "Fetching name...".to_string(),
                                    magnet_link.trim().to_string(),
                                    Some(download_path),
                                    false,
                                    TorrentControlState::Running,
                                    HashMap::new(),
                                    None,
                                )
                                .await;
                                self.save_state_to_disk();
                            } else {
                                self.app_state.pending_torrent_link = magnet_link;
                                let initial_path = self.get_initial_destination_path();

                                let _ = self.app_command_tx.try_send(AppCommand::FetchFileTree {
                                    path: initial_path,
                                    browser_mode: FileBrowserMode::DownloadLocSelection {
                                        torrent_files: vec![],
                                        container_name: "Magnet Download".to_string(), // Default name for magnets
                                        use_container: true,
                                        is_editing_name: false,
                                        preview_tree: Vec::new(), // Magnets start with empty metadata
                                        preview_state: TreeViewState::default(),
                                        focused_pane: BrowserPane::FileSystem,
                                        cursor_pos: 0,
                                        original_name_backup: "Magnet Download".to_string(),
                                    },
                                    highlight_path: None,
                                });
                            }
                        }
                        Err(e) => {
                            tracing_event!(
                                Level::ERROR,
                                "Failed to read magnet file {:?}: {}",
                                &path,
                                e
                            );
                        }
                    }

                    if let Err(e) = fs::create_dir_all(&processed_path) {
                        tracing_event!(
                            Level::ERROR,
                            "Could not create processed files directory: {}",
                            e
                        );
                    } else if let Some(file_name) = path.file_name() {
                        let new_path = processed_path.join(file_name);
                        if let Err(e) = fs::rename(&path, &new_path) {
                            tracing_event!(
                                Level::ERROR,
                                "Failed to move processed magnet file {:?}: {}",
                                &path,
                                e
                            );
                        }
                    }
                } else {
                    tracing_event!(
                        Level::ERROR,
                        "Could not get system watch paths for magnet processing."
                    );
                }
            }
            AppCommand::ClientShutdown(path) => {
                tracing_event!(Level::INFO, "Shutdown command received via command file.");
                self.app_state.should_quit = true;
                if let Err(e) = fs::remove_file(&path) {
                    tracing_event!(
                        Level::WARN,
                        "Failed to remove command file {:?}: {}",
                        &path,
                        e
                    );
                }
            }
            AppCommand::PortFileChanged(path) => {
                self.handle_port_change(path).await;
            }

            AppCommand::FetchFileTree {
                path,
                browser_mode,
                highlight_path,
            } => {
                let tx = self.app_command_tx.clone();
                let mut shutdown_rx = self.shutdown_tx.subscribe();
                let path_clone = path.clone();
                let highlight_clone = highlight_path.clone();

                // 1. Update or Initialize the UI state immediately
                if matches!(self.app_state.mode, AppMode::FileBrowser) {
                    // If already in browser, just update the path we are viewing
                    self.app_state.ui.file_browser.state.current_path = path.clone();
                    self.app_state.ui.file_browser.browser_mode = browser_mode;
                } else {
                    // Otherwise, initialize the mode
                    let mut tree_state = crate::tui::tree::TreeViewState::new();
                    tree_state.current_path = path.clone();
                    self.app_state.ui.file_browser.state = tree_state;
                    self.app_state.ui.file_browser.data = Vec::new();
                    self.app_state.ui.file_browser.browser_mode = browser_mode;
                    self.app_state.mode = AppMode::FileBrowser;
                }

                // 2. Spawn the background crawl
                tokio::spawn(async move {
                    tokio::select! {
                        result = build_fs_tree(&path_clone, 0) => {
                            if let Ok(nodes) = result {
                                // Pass the highlight_path back so the Update arm can find it
                                let _ = tx.send(AppCommand::UpdateFileBrowserData {
                                    data: nodes,
                                    highlight_path: highlight_clone
                                }).await;
                            }
                        }
                        _ = shutdown_rx.recv() => {
                            tracing::debug!("Aborting FileBrowser crawl due to shutdown");
                        }
                    }
                });
            }

            AppCommand::UpdateFileBrowserData {
                mut data,
                highlight_path,
            } => {
                if matches!(self.app_state.mode, AppMode::FileBrowser) {
                    let state = &mut self.app_state.ui.file_browser.state;
                    let existing_data = &mut self.app_state.ui.file_browser.data;
                    let browser_mode = &mut self.app_state.ui.file_browser.browser_mode;
                    // --- 1. Apply Dynamic Sorting ---
                    if let FileBrowserMode::File(extensions) = browser_mode {
                        let target_exts: Vec<String> =
                            extensions.iter().map(|e| e.to_lowercase()).collect();
                        let has_target_files = data.iter().any(|node| {
                            !node.is_dir
                                && target_exts
                                    .iter()
                                    .any(|ext| node.name.to_lowercase().ends_with(ext))
                        });

                        if !has_target_files {
                            data.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
                        } else {
                            data.sort_by(|a, b| {
                                let a_matches = target_exts
                                    .iter()
                                    .any(|ext| a.name.to_lowercase().ends_with(ext));
                                let b_matches = target_exts
                                    .iter()
                                    .any(|ext| b.name.to_lowercase().ends_with(ext));

                                // 1. Priority: Torrents first
                                if a_matches != b_matches {
                                    return b_matches.cmp(&a_matches);
                                }

                                // 2. Priority: Folders second (ensures folders follow torrents directly)
                                if a.is_dir != b.is_dir {
                                    return b.is_dir.cmp(&a.is_dir); // Changed order to put folders higher
                                }

                                // 3. Final: Sort by newest date
                                b.payload.modified.cmp(&a.payload.modified)
                            });
                        }
                    }

                    // --- 2. Update Data ---
                    *existing_data = data;
                    state.top_most_offset = 0;

                    // --- 3. Smart Cursor Positioning ---
                    if let Some(target) = highlight_path {
                        // Find the index of the folder/file we want to highlight
                        if let Some(index) = existing_data
                            .iter()
                            .position(|node| node.full_path == target)
                        {
                            state.cursor_path = Some(target);

                            // Adjust scroll if the item is below the current visible area
                            let area = crate::tui::formatters::centered_rect(
                                75,
                                80,
                                self.app_state.screen_area,
                            );
                            let max_height = area.height.saturating_sub(2) as usize;
                            if index >= max_height {
                                state.top_most_offset = index.saturating_sub(max_height / 2);
                            }
                        } else {
                            state.cursor_path =
                                existing_data.first().map(|node| node.full_path.clone());
                        }
                    } else {
                        // Default: reset to top if entering a new folder
                        state.cursor_path =
                            existing_data.first().map(|node| node.full_path.clone());
                    }

                    self.app_state.ui.needs_redraw = true;
                }
            }
            AppCommand::RssSyncNow => {
                let _ = self.rss_sync_tx.try_send(());
                self.app_state.ui.needs_redraw = true;
            }
            AppCommand::RssPreviewUpdated(preview_items) => {
                self.app_state.rss_runtime.preview_items = preview_items;
                self.refresh_rss_derived();
                self.app_state.ui.needs_redraw = true;
            }
            AppCommand::RssSyncStatusUpdated {
                last_sync_at,
                next_sync_at,
            } => {
                self.app_state.rss_runtime.last_sync_at = last_sync_at;
                self.app_state.rss_runtime.next_sync_at = next_sync_at;
                self.save_state_to_disk();
                self.app_state.ui.needs_redraw = true;
            }
            AppCommand::RssFeedErrorUpdated { feed_url, error } => {
                if let Some(err) = error {
                    self.app_state.rss_runtime.feed_errors.insert(feed_url, err);
                } else {
                    self.app_state.rss_runtime.feed_errors.remove(&feed_url);
                }
                self.save_state_to_disk();
                self.app_state.ui.needs_redraw = true;
            }
            AppCommand::RssDownloadSelected(entry) => {
                let existing_idx = self
                    .app_state
                    .rss_runtime
                    .history
                    .iter()
                    .position(|existing| existing.dedupe_key == entry.dedupe_key);
                if let Some(idx) = existing_idx {
                    if self.app_state.rss_runtime.history[idx].info_hash.is_none()
                        && entry.info_hash.is_some()
                    {
                        self.app_state.rss_runtime.history[idx].info_hash = entry.info_hash.clone();
                        self.save_state_to_disk();
                    }
                } else {
                    self.app_state.rss_runtime.history.push(entry);
                    self.save_state_to_disk();
                }
                self.refresh_rss_derived();
                self.app_state.ui.needs_redraw = true;
            }
            AppCommand::RssDownloadPreview(item) => {
                self.download_rss_preview_item(item).await;
                self.refresh_rss_derived();
                self.app_state.ui.needs_redraw = true;
            }
            AppCommand::NetworkHistoryLoaded(state) => {
                NetworkHistoryTelemetry::apply_loaded_state(&mut self.app_state, state);
                self.app_state.network_history_restore_pending = false;
                self.app_state.ui.needs_redraw = true;
            }
            AppCommand::NetworkHistoryPersisted {
                request_id,
                success,
            } => {
                apply_network_history_persist_result(&mut self.app_state, request_id, success);
            }
            AppCommand::UpdateConfig(new_settings) => {
                let old_settings = self.client_configs.clone();
                self.client_configs = new_settings.clone();
                let _ = self.rss_settings_tx.send(self.client_configs.clone());
                let rss_changed = rss_settings_changed(&old_settings, &new_settings);

                if new_settings.ui_theme != old_settings.ui_theme {
                    self.app_state.theme = Theme::builtin(new_settings.ui_theme);
                }

                // 1. Handle Port Change (Re-bind Listener)
                if new_settings.client_port != old_settings.client_port {
                    tracing::info!(
                        "Config update: Port changed to {}",
                        new_settings.client_port
                    );
                    // Reuse your existing port logic or extract it to a helper
                    self.rebind_listener(new_settings.client_port).await;
                }

                // 2. Handle Bandwidth Limit Changes (Update Buckets)
                if new_settings.global_download_limit_bps != old_settings.global_download_limit_bps
                {
                    self.global_dl_bucket
                        .set_rate(new_settings.global_download_limit_bps as f64);
                }
                if new_settings.global_upload_limit_bps != old_settings.global_upload_limit_bps {
                    self.global_ul_bucket
                        .set_rate(new_settings.global_upload_limit_bps as f64);
                }

                if new_settings.watch_folder != old_settings.watch_folder {
                    if let Some(old_path) = old_settings.watch_folder {
                        if let Err(e) = self.watcher.unwatch(&old_path) {
                            tracing::info!("Failed to unwatch old folder {:?}: {}", old_path, e);
                        }
                    }

                    if let Some(new_path) = &self.client_configs.watch_folder {
                        if let Err(e) = self.watcher.watch(new_path, RecursiveMode::NonRecursive) {
                            tracing::error!("Failed to watch new folder: {}", e);
                        }
                    }
                }

                // Refresh RSS preview immediately when feed/filter config changes.
                if rss_changed {
                    prune_rss_feed_errors(
                        &mut self.app_state.rss_runtime.feed_errors,
                        &self.client_configs,
                    );
                    self.refresh_rss_derived();
                    let _ = self.rss_sync_tx.try_send(());
                }

                // 3. Persist to Disk
                self.save_state_to_disk();

                // 4. Force Redraw
                self.app_state.ui.needs_redraw = true;
            }

            AppCommand::UpdateVersionAvailable(latest_version) => {
                self.app_state.update_available = Some(latest_version);
            }
        }
    }

    fn handle_manager_event(&mut self, event: ManagerEvent) {
        if UiTelemetry::on_manager_event_metrics(&mut self.app_state, &event) {
            return;
        }

        match event {
            ManagerEvent::DeletionComplete(info_hash, result) => {
                if let Err(e) = result {
                    tracing_event!(Level::ERROR, "Deletion failed for torrent: {}", e);
                }

                self.client_configs.torrents.retain(|t| {
                    let t_info_hash = if t.torrent_or_magnet.starts_with("magnet:") {
                        Magnet::new(&t.torrent_or_magnet)
                            .ok()
                            .and_then(|m| m.hash().map(|s| s.to_string()))
                            .and_then(|hash_str| decode_info_hash(&hash_str).ok())
                    } else {
                        PathBuf::from(&t.torrent_or_magnet)
                            .file_stem()
                            .and_then(|s| s.to_str())
                            .and_then(|s| hex::decode(s).ok())
                    };

                    match t_info_hash {
                        Some(t_hash) => t_hash != info_hash,
                        None => true,
                    }
                });

                self.app_state.torrents.remove(&info_hash);
                self.torrent_manager_command_txs.remove(&info_hash);
                self.torrent_manager_incoming_peer_txs.remove(&info_hash);
                self.torrent_metric_watch_rxs.remove(&info_hash);
                self.app_state
                    .torrent_list_order
                    .retain(|ih| *ih != info_hash);

                if self.app_state.ui.selected_torrent_index
                    >= self.app_state.torrent_list_order.len()
                    && !self.app_state.torrent_list_order.is_empty()
                {
                    self.app_state.ui.selected_torrent_index =
                        self.app_state.torrent_list_order.len() - 1;
                }

                self.save_state_to_disk();
                self.refresh_rss_derived();

                self.app_state.ui.needs_redraw = true;
            }
            ManagerEvent::MetadataLoaded { info_hash, torrent } => {
                if let FileBrowserMode::DownloadLocSelection {
                    preview_tree,
                    preview_state,
                    container_name,
                    original_name_backup,
                    use_container,
                    ..
                } = &mut self.app_state.ui.file_browser.browser_mode
                {
                    // 1. REDUNDANCY GUARD: Check if metadata was already processed
                    // If the tree is already populated, ignore subsequent peer metadata arrivals
                    if !preview_tree.is_empty() {
                        tracing::debug!(target: "superseedr", "Metadata already hydrated for {:?}, ignoring redundant peer update", hex::encode(&info_hash));
                        return;
                    }

                    // 2. Build the tree payloads
                    let file_list = torrent.file_list();
                    let payloads: Vec<(Vec<String>, TorrentPreviewPayload)> = file_list
                        .into_iter()
                        .enumerate()
                        .map(|(idx, (parts, size))| {
                            (
                                parts,
                                TorrentPreviewPayload {
                                    file_index: Some(idx),
                                    size,
                                    priority: FilePriority::Normal,
                                },
                            )
                        })
                        .collect();

                    // 3. Hydrate the tree structure
                    let has_multiple_files = payloads.len() > 1;
                    *preview_tree = RawNode::from_path_list(None, payloads);

                    // 4. Update Display Name and State
                    let info_hash_hex = hex::encode(&info_hash);
                    let name = format!("{} [{}]", torrent.info.name, &info_hash_hex);
                    *container_name = name.clone();
                    *original_name_backup = name;
                    *use_container = has_multiple_files;

                    // 5. INITIALIZE UI STATE: Set the initial cursor
                    if let Some(first) = preview_tree.first() {
                        preview_state.cursor_path = Some(std::path::PathBuf::from(&first.name));
                    }

                    // 6. Auto-expand all folders
                    for node in preview_tree.iter_mut() {
                        node.expand_all(preview_state);
                    }

                    // 7. Force UI redraw
                    self.app_state.ui.needs_redraw = true;
                    tracing::info!(target: "superseedr", "Magnet preview tree hydrated (first arrival)");
                }
            }
            ManagerEvent::DiskReadStarted { .. }
            | ManagerEvent::DiskReadFinished
            | ManagerEvent::DiskWriteStarted { .. }
            | ManagerEvent::DiskWriteFinished
            | ManagerEvent::DiskIoBackoff { .. }
            | ManagerEvent::PeerDiscovered { .. }
            | ManagerEvent::PeerConnected { .. }
            | ManagerEvent::PeerDisconnected { .. }
            | ManagerEvent::BlockReceived { .. }
            | ManagerEvent::BlockSent { .. } => {}
        }
    }

    async fn handle_file_event(&mut self, result: Result<Event, notify::Error>) {
        match result {
            Ok(event) => {
                const DEBOUNCE_DURATION: Duration = Duration::from_millis(500);
                for path in event.paths {
                    if path.to_string_lossy().ends_with(".tmp") {
                        continue;
                    }

                    let now = Instant::now();
                    if let Some(last_time) = self.app_state.recently_processed_files.get(&path) {
                        if now.duration_since(*last_time) < DEBOUNCE_DURATION {
                            continue;
                        }
                    }

                    self.app_state
                        .recently_processed_files
                        .insert(path.clone(), now);

                    // Use externalized logic for mapping path/event to command.
                    // Note: event_to_commands could be used, but since we are already looping and debouncing,
                    // we can just use the path-to-command logic if we expose it or just use event_to_commands as a batch.
                    if let Some(cmd) = watcher::path_to_command(&path) {
                        let _ = self.app_command_tx.send(cmd).await;
                    }
                }
            }
            Err(e) => {
                tracing_event!(Level::ERROR, "File watcher error: {}", e);
            }
        }
    }

    async fn handle_port_change(&mut self, path: PathBuf) {
        tracing_event!(Level::DEBUG, "Processing port file change...");
        let port_str = match fs::read_to_string(&path) {
            Ok(s) => s,
            Err(e) => {
                tracing_event!(Level::ERROR, "Failed to read port file {:?}: {}", &path, e);
                return;
            }
        };

        match port_str.trim().parse::<u16>() {
            Ok(new_port) => {
                if new_port > 0 && new_port != self.client_configs.client_port {
                    tracing_event!(
                        Level::INFO,
                        "Port changed: {} -> {}. Attempting to re-bind listener.",
                        self.client_configs.client_port,
                        new_port
                    );

                    match tokio::net::TcpListener::bind(format!("0.0.0.0:{}", new_port)).await {
                        Ok(new_listener) => {
                            self.listener = new_listener;
                            self.client_configs.client_port = new_port;

                            tracing_event!(
                                Level::INFO,
                                "Successfully bound to new port {}",
                                new_port
                            );

                            // Persist the new port immediately
                            self.save_state_to_disk();

                            // Notify all running managers
                            for manager_tx in self.torrent_manager_command_txs.values() {
                                let _ =
                                    manager_tx.try_send(ManagerCommand::UpdateListenPort(new_port));
                            }

                            // Rebuild DHT if enabled
                            #[cfg(feature = "dht")]
                            {
                                tracing::event!(Level::INFO, "Rebinding DHT server to new port...");
                                let bootstrap_nodes: Vec<&str> = self
                                    .client_configs
                                    .bootstrap_nodes
                                    .iter()
                                    .map(AsRef::as_ref)
                                    .collect();

                                match Dht::builder()
                                    .bootstrap(&bootstrap_nodes)
                                    .port(new_port)
                                    .server_mode()
                                    .build()
                                {
                                    Ok(new_dht_server) => {
                                        let new_dht_handle = new_dht_server.as_async();
                                        self.distributed_hash_table = new_dht_handle.clone();

                                        for manager_tx in self.torrent_manager_command_txs.values()
                                        {
                                            let _ = manager_tx.try_send(
                                                ManagerCommand::UpdateDhtHandle(
                                                    new_dht_handle.clone(),
                                                ),
                                            );
                                        }
                                        self.dht_bootstrap_warning = None;
                                        self.refresh_system_warning();
                                        tracing::event!(
                                            Level::INFO,
                                            "DHT server rebound and handles updated."
                                        );
                                    }
                                    Err(e) => {
                                        self.dht_bootstrap_warning = Some(format!(
                                            "Warning: DHT bootstrap unavailable ({}). Running without bootstrap; retrying automatically.",
                                            e
                                        ));
                                        self.refresh_system_warning();
                                        tracing::event!(
                                            Level::ERROR,
                                            "Failed to build new DHT server: {}",
                                            e
                                        );
                                    }
                                }
                            }
                        }
                        Err(e) => {
                            tracing_event!(
                                Level::ERROR,
                                "Failed to bind to new port {}: {}. Retaining old listener.",
                                new_port,
                                e
                            );
                        }
                    }
                } else if new_port == self.client_configs.client_port {
                    tracing_event!(
                        Level::DEBUG,
                        "Port file updated, but port is unchanged ({}).",
                        new_port
                    );
                }
            }
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "Failed to parse new port from file {:?}: {}",
                    &path,
                    e
                );
            }
        }
    }

    fn calculate_stats(&mut self, sys: &mut System) {
        let was_seeding = self.app_state.is_seeding;
        UiTelemetry::on_second_tick(&mut self.app_state, sys);
        NetworkHistoryTelemetry::on_second_tick(&mut self.app_state);
        self.tuning_controller.on_second_tick();
        self.app_state.tuning_countdown = self.tuning_controller.countdown_secs();
        if was_seeding != self.app_state.is_seeding {
            self.reset_tuning_for_objective_change();

            let rm = self.resource_manager.clone();
            let limits_map = self.app_state.limits.clone().into_map();
            tokio::spawn(async move {
                let _ = rm.update_limits(limits_map).await;
            });
        }

        let history = if !self.app_state.is_seeding {
            &self.app_state.avg_download_history
        } else {
            &self.app_state.avg_upload_history
        };
        let lookback = self.tuning_controller.lookback_secs();
        let relevant_history = &history[history.len().saturating_sub(lookback)..];
        self.tuning_controller.update_live_score(
            relevant_history,
            self.app_state.global_disk_thrash_score,
            self.app_state.adaptive_max_scpb,
        );
        self.sync_tuning_state_from_controller();
    }

    fn startup_network_history_restore(&mut self) {
        self.app_state.network_history_restore_pending = true;
        let tx = self.app_command_tx.clone();
        tokio::spawn(async move {
            let load_result = tokio::task::spawn_blocking(load_network_history_state).await;
            match load_result {
                Ok(state) => {
                    let _ = tx.send(AppCommand::NetworkHistoryLoaded(state)).await;
                }
                Err(e) => {
                    tracing_event!(
                        Level::ERROR,
                        "Network history restore task failed to join: {}",
                        e
                    );
                    let _ = tx
                        .send(AppCommand::NetworkHistoryLoaded(
                            NetworkHistoryPersistedState::default(),
                        ))
                        .await;
                }
            }
        });
    }

    fn drain_latest_torrent_metrics(&mut self) {
        let mut changed = false;
        let mut closed_info_hashes = Vec::new();

        for (info_hash, rx) in self.torrent_metric_watch_rxs.iter_mut() {
            match rx.has_changed() {
                Ok(false) => {}
                Ok(true) => {
                    let message = rx.borrow_and_update().clone();
                    UiTelemetry::on_metrics(&mut self.app_state, message);
                    changed = true;
                }
                Err(_) => {
                    closed_info_hashes.push(info_hash.clone());
                }
            }
        }

        for info_hash in closed_info_hashes {
            self.torrent_metric_watch_rxs.remove(&info_hash);
        }

        if changed {
            self.sort_and_filter_torrent_list();
            // Keep RSS derived recomputation off the hot metrics path.
            // Full recompute is done on structural RSS changes (preview/filter/history/add/remove/search/edit).
            self.app_state.ui.needs_redraw = true;
        }
    }

    async fn tuning_resource_limits(&mut self) {
        let history = if !self.app_state.is_seeding {
            &self.app_state.avg_download_history
        } else {
            &self.app_state.avg_upload_history
        };

        let lookback = self.tuning_controller.lookback_secs();
        let relevant_history = &history[history.len().saturating_sub(lookback)..];
        let evaluation = self.tuning_controller.evaluate_cycle(
            &self.app_state.limits,
            relevant_history,
            self.app_state.global_disk_thrash_score,
            self.app_state.adaptive_max_scpb,
        );
        self.sync_tuning_state_from_controller();

        if evaluation.accepted_improvement {
            tracing_event!(
                Level::DEBUG,
                "Self-Tune: SUCCESS. New best score: {} (raw: {}, penalty: {:.2}x)",
                evaluation.new_score,
                evaluation.new_raw_score,
                evaluation.penalty_factor
            );
        } else {
            self.app_state.limits = evaluation.effective_limits.clone();
            if evaluation.reality_check_applied {
                tracing_event!(Level::DEBUG, "Self-Tune: REALITY CHECK. Score {} (raw: {}) failed. Old best {} is stale vs. baseline {}. Resetting best to baseline.", evaluation.new_score, evaluation.new_raw_score, evaluation.best_score_before, evaluation.baseline_u64);
            } else {
                tracing_event!(Level::DEBUG, "Self-Tune: REVERTING. Score {} (raw: {}, penalty: {:.2}x) was not better than {}. (Baseline is {})", evaluation.new_score, evaluation.new_raw_score, evaluation.penalty_factor, evaluation.best_score_before, evaluation.baseline_u64);
            }

            let _ = self
                .resource_manager
                .update_limits(self.app_state.limits.clone().into_map())
                .await;
        }

        let (next_limits, desc) =
            make_random_adjustment(self.app_state.limits.clone(), self.app_state.is_seeding);
        self.app_state.limits = next_limits;

        tracing_event!(Level::DEBUG, "Self-Tune: Trying next change... {}", desc);
        let _ = self
            .resource_manager
            .update_limits(self.app_state.limits.clone().into_map())
            .await;
    }

    fn reschedule_tuning_deadline(&mut self) {
        self.next_tuning_at =
            time::Instant::now() + Duration::from_secs(self.tuning_controller.cadence_secs());
    }

    fn reset_tuning_for_objective_change(&mut self) {
        self.app_state.limits =
            normalize_limits_for_mode(&self.app_state.limits, self.app_state.is_seeding);
        self.tuning_controller
            .reset_for_objective_change(&self.app_state.limits);
        self.sync_tuning_state_from_controller();
        self.reschedule_tuning_deadline();
    }

    fn sync_tuning_state_from_controller(&mut self) {
        let state = self.tuning_controller.state();
        self.app_state.last_tuning_score = state.last_tuning_score;
        self.app_state.current_tuning_score = state.current_tuning_score;
        self.app_state.last_tuning_limits = state.last_tuning_limits.clone();
        self.app_state.baseline_speed_ema = state.baseline_speed_ema;
        self.app_state.tuning_countdown = self.tuning_controller.countdown_secs();
    }

    fn save_state_to_disk(&mut self) {
        let payload = build_persist_payload(&mut self.client_configs, &mut self.app_state);
        let network_history_request_id = payload
            .network_history
            .as_ref()
            .map(|request| request.request_id);

        if queue_persistence_payload(self.persistence_tx.as_ref(), payload).is_ok() {
            self.app_state.pending_network_history_persist_request_id = network_history_request_id;
        } else {
            tracing_event!(
                Level::ERROR,
                "Failed to queue persistence payload: persistence task unavailable"
            );
        }
    }

    // Constantly ensures all table selected indices are in-bounds
    fn clamp_selected_indices(&mut self) {
        clamp_selected_indices_in_state(&mut self.app_state);
    }

    pub fn sort_and_filter_torrent_list(&mut self) {
        sort_and_filter_torrent_list_state(&mut self.app_state);
    }

    pub fn find_most_common_download_path(&mut self) -> Option<PathBuf> {
        let mut counts: HashMap<PathBuf, usize> = HashMap::new();

        for state in self.app_state.torrents.values() {
            if let Some(download_path) = &state.latest_state.download_path {
                if let Some(parent_path) = download_path.parent() {
                    *counts.entry(parent_path.to_path_buf()).or_insert(0) += 1;
                }
            }
        }

        counts
            .into_iter()
            .max_by_key(|&(_, count)| count)
            .map(|(path, _)| path)
    }

    pub fn get_initial_source_path(&self) -> PathBuf {
        UserDirs::new()
            .and_then(|ud| ud.download_dir().map(|p| p.to_path_buf()))
            .or_else(|| UserDirs::new().map(|ud| ud.home_dir().to_path_buf()))
            .unwrap_or_else(|| PathBuf::from("/"))
    }

    pub fn get_initial_destination_path(&mut self) -> PathBuf {
        self.find_most_common_download_path()
            .or_else(|| UserDirs::new().and_then(|ud| ud.download_dir().map(|p| p.to_path_buf())))
            .or_else(|| UserDirs::new().map(|ud| ud.home_dir().to_path_buf()))
            .unwrap_or_else(|| PathBuf::from("/"))
    }

    pub async fn add_torrent_from_file(
        &mut self,
        path: PathBuf,
        download_path: Option<PathBuf>,
        is_validated: bool,
        torrent_control_state: TorrentControlState,
        file_priorities: HashMap<usize, FilePriority>,
        container_name: Option<String>,
    ) {
        let buffer = match fs::read(&path) {
            Ok(buf) => buf,
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "Failed to read torrent file {:?}: {}",
                    &path,
                    e
                );
                return;
            }
        };

        let torrent = match from_bytes(&buffer) {
            Ok(t) => t,
            Err(e) => {
                let file_size = buffer.len();
                let head_len = file_size.min(24);
                let tail_len = file_size.min(24);
                let head_hex = hex::encode(&buffer[..head_len]);
                let tail_hex = hex::encode(&buffer[file_size.saturating_sub(tail_len)..]);
                let likely_cause = if e.to_string().contains("End of stream") {
                    "likely truncated/incomplete .torrent file"
                } else {
                    "malformed or unsupported bencode payload"
                };
                tracing_event!(
                    Level::ERROR,
                    "Failed to parse torrent file {:?}: {} | size={} bytes | head={} | tail={} | hint={}",
                    &path,
                    e,
                    file_size,
                    head_hex,
                    tail_hex,
                    likely_cause
                );
                return;
            }
        };

        #[cfg(all(feature = "dht", feature = "pex"))]
        {
            if torrent.info.private == Some(1) {
                tracing_event!(
                    Level::ERROR,
                    "Rejected private torrent '{}' in normal build.",
                    torrent.info.name
                );
                self.app_state.system_error = Some(format!(
                    "Private Torrent Rejected:'{}' This build (with DHT/PEX) is not safe for private trackers. Please use private builds for this torrent.",
                    torrent.info.name
                ));
                return;
            }
        }

        let info_hash = if torrent.info.meta_version == Some(2) {
            if !torrent.info.pieces.is_empty() {
                let mut hasher = sha1::Sha1::new();
                hasher.update(&torrent.info_dict_bencode);
                hasher.finalize().to_vec()
            } else {
                // Pure V2 -> Primary is V2 (SHA-256 Truncated)
                let mut hasher = Sha256::new();
                hasher.update(&torrent.info_dict_bencode);
                hasher.finalize()[0..20].to_vec()
            }
        } else {
            // V1 -> SHA-1
            let mut hasher = sha1::Sha1::new();
            hasher.update(&torrent.info_dict_bencode);
            hasher.finalize().to_vec()
        };

        if self.app_state.torrents.contains_key(&info_hash) {
            tracing_event!(
                Level::INFO,
                "Ignoring already present torrent: {}",
                torrent.info.name
            );
            return;
        }

        let torrent_files_dir = match get_app_paths() {
            Some((_, data_dir)) => data_dir.join("torrents"),
            None => {
                tracing_event!(
                    Level::ERROR,
                    "Could not determine application data directory."
                );
                return;
            }
        };
        if let Err(e) = fs::create_dir_all(&torrent_files_dir) {
            tracing_event!(
                Level::ERROR,
                "Could not create torrents data directory: {}",
                e
            );
            return;
        }
        let permanent_torrent_path =
            torrent_files_dir.join(format!("{}.torrent", hex::encode(&info_hash)));
        // Persist from in-memory bytes via temp+rename to avoid self-copy corruption
        // when `path` already points at `permanent_torrent_path` during startup reload.
        let temp_torrent_path = torrent_files_dir.join(format!(
            "{}.{}.tmp",
            hex::encode(&info_hash),
            std::process::id()
        ));
        if let Err(e) = fs::write(&temp_torrent_path, &buffer) {
            tracing_event!(
                Level::ERROR,
                "Failed to write temp torrent in data directory: {}",
                e
            );
            return;
        }
        if let Err(e) = fs::rename(&temp_torrent_path, &permanent_torrent_path) {
            if e.kind() == ErrorKind::AlreadyExists {
                if let Err(remove_err) = fs::remove_file(&permanent_torrent_path) {
                    if remove_err.kind() != ErrorKind::NotFound {
                        let _ = fs::remove_file(&temp_torrent_path);
                        tracing_event!(
                            Level::ERROR,
                            "Failed to replace existing torrent file in data directory: {}",
                            remove_err
                        );
                        return;
                    }
                }

                if let Err(retry_err) = fs::rename(&temp_torrent_path, &permanent_torrent_path) {
                    let _ = fs::remove_file(&temp_torrent_path);
                    tracing_event!(
                        Level::ERROR,
                        "Failed to finalize torrent copy in data directory after replace: {}",
                        retry_err
                    );
                    return;
                }
            } else {
                let _ = fs::remove_file(&temp_torrent_path);
                tracing_event!(
                    Level::ERROR,
                    "Failed to finalize torrent copy in data directory: {}",
                    e
                );
                return;
            }
        }

        let number_of_pieces_total = if !torrent.info.pieces.is_empty() {
            (torrent.info.pieces.len() / 20) as u32
        } else {
            // Handle v2 torrents (empty pieces list)
            let total_len = torrent.info.total_length();
            if torrent.info.piece_length > 0 {
                // ceil(total_len / piece_length)
                ((total_len as f64) / (torrent.info.piece_length as f64)).ceil() as u32
            } else {
                0
            }
        };

        let placeholder_state = TorrentDisplayState {
            latest_state: TorrentMetrics {
                torrent_control_state: torrent_control_state.clone(),
                info_hash: info_hash.clone(),
                torrent_or_magnet: permanent_torrent_path.to_string_lossy().to_string(),
                torrent_name: torrent.info.name.clone(),
                download_path: download_path.clone(),
                container_name: container_name.clone(),
                number_of_pieces_total,
                file_priorities: file_priorities.clone(),
                ..Default::default()
            },
            ..Default::default()
        };
        self.app_state
            .torrents
            .insert(info_hash.clone(), placeholder_state);
        self.app_state.torrent_list_order.push(info_hash.clone());
        self.refresh_rss_derived();

        if matches!(self.app_state.mode, AppMode::Welcome) {
            self.app_state.mode = AppMode::Normal;
        }

        let (incoming_peer_tx, incoming_peer_rx) = mpsc::channel::<(TcpStream, Vec<u8>)>(100);
        self.torrent_manager_incoming_peer_txs
            .insert(info_hash.clone(), incoming_peer_tx);
        let (manager_command_tx, manager_command_rx) = mpsc::channel::<ManagerCommand>(100);
        self.torrent_manager_command_txs
            .insert(info_hash.clone(), manager_command_tx);

        let (torrent_metrics_tx, torrent_metrics_rx) = watch::channel(TorrentMetrics::default());
        self.torrent_metric_watch_rxs
            .insert(info_hash.clone(), torrent_metrics_rx);
        let manager_event_tx_clone = self.manager_event_tx.clone();
        let resource_manager_clone = self.resource_manager.clone();
        let global_dl_bucket_clone = self.global_dl_bucket.clone();
        let global_ul_bucket_clone = self.global_ul_bucket.clone();

        #[cfg(feature = "dht")]
        let dht_clone = self.distributed_hash_table.clone();
        #[cfg(not(feature = "dht"))]
        let dht_clone = ();

        let torrent_params = TorrentParameters {
            dht_handle: dht_clone,
            incoming_peer_rx,
            metrics_tx: torrent_metrics_tx,
            torrent_validation_status: is_validated,
            torrent_data_path: download_path,
            container_name: container_name.clone(),
            manager_command_rx,
            manager_event_tx: manager_event_tx_clone,
            settings: Arc::clone(&Arc::new(self.client_configs.clone())),
            resource_manager: resource_manager_clone,
            global_dl_bucket: global_dl_bucket_clone,
            global_ul_bucket: global_ul_bucket_clone,
            file_priorities: file_priorities.clone(),
        };

        match TorrentManager::from_torrent(torrent_params, torrent) {
            Ok(torrent_manager) => {
                tokio::spawn(async move {
                    let _ = torrent_manager
                        .run(torrent_control_state == TorrentControlState::Paused)
                        .await;
                });
            }
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "Failed to create torrent manager from file: {:?}",
                    e
                );
                self.app_state.torrents.remove(&info_hash);
                self.app_state
                    .torrent_list_order
                    .retain(|ih| *ih != info_hash);
                self.torrent_metric_watch_rxs.remove(&info_hash);
                self.refresh_rss_derived();
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn add_magnet_torrent(
        &mut self,
        torrent_name: String,
        magnet_link: String,
        download_path: Option<PathBuf>,
        is_validated: bool,
        torrent_control_state: TorrentControlState,
        file_priorities: HashMap<usize, FilePriority>,
        container_name: Option<String>,
    ) {
        tracing::info!(target: "magnet_flow", "Engine: add_magnet_torrent entry. Link: {}", magnet_link);
        let magnet = match Magnet::new(&magnet_link) {
            Ok(m) => m,
            Err(e) => {
                tracing_event!(Level::ERROR, "Could not parse invalid magnet: {:?}", e);
                return;
            }
        };

        let (v1_hash, v2_hash) = parse_hybrid_hashes(&magnet_link);
        let info_hash = v1_hash
            .clone()
            .or_else(|| v2_hash.clone())
            .expect("Magnet link missing both btih and btmh hashes");
        let resolved_name = resolve_magnet_torrent_name(&torrent_name, &magnet_link, &info_hash);

        if self.app_state.torrents.contains_key(&info_hash) {
            if let Some(path) = download_path {
                if let Some(manager_tx) = self.torrent_manager_command_txs.get(&info_hash) {
                    let _ = manager_tx.try_send(ManagerCommand::SetUserTorrentConfig {
                        torrent_data_path: path,
                        file_priorities: file_priorities.clone(),
                        container_name,
                    });
                }
            }
            tracing_event!(Level::INFO, "Updated path for existing torrent from magnet");
            return;
        }

        let placeholder_state = TorrentDisplayState {
            latest_state: TorrentMetrics {
                torrent_control_state: torrent_control_state.clone(),
                info_hash: info_hash.clone(),
                torrent_or_magnet: magnet_link.clone(),
                torrent_name: resolved_name,
                download_path: download_path.clone(),
                container_name: container_name.clone(),
                ..Default::default()
            },
            ..Default::default()
        };
        self.app_state
            .torrents
            .insert(info_hash.clone(), placeholder_state);
        self.app_state.torrent_list_order.push(info_hash.clone());
        self.refresh_rss_derived();

        if matches!(self.app_state.mode, AppMode::Welcome) {
            self.app_state.mode = AppMode::Normal;
        }

        let (incoming_peer_tx, incoming_peer_rx) = mpsc::channel::<(TcpStream, Vec<u8>)>(100);
        self.torrent_manager_incoming_peer_txs
            .insert(info_hash.clone(), incoming_peer_tx);
        let (manager_command_tx, manager_command_rx) = mpsc::channel::<ManagerCommand>(100);
        self.torrent_manager_command_txs
            .insert(info_hash.clone(), manager_command_tx);

        let dht_clone = self.distributed_hash_table.clone();
        let (torrent_metrics_tx, torrent_metrics_rx) = watch::channel(TorrentMetrics::default());
        self.torrent_metric_watch_rxs
            .insert(info_hash.clone(), torrent_metrics_rx);
        let manager_event_tx_clone = self.manager_event_tx.clone();
        let resource_manager_clone = self.resource_manager.clone();
        let global_dl_bucket_clone = self.global_dl_bucket.clone();
        let global_ul_bucket_clone = self.global_ul_bucket.clone();
        let torrent_params = TorrentParameters {
            dht_handle: dht_clone,
            incoming_peer_rx,
            metrics_tx: torrent_metrics_tx,
            torrent_validation_status: is_validated,
            torrent_data_path: download_path.clone(),
            container_name: container_name.clone(),
            manager_command_rx,
            manager_event_tx: manager_event_tx_clone,
            settings: Arc::clone(&Arc::new(self.client_configs.clone())),
            resource_manager: resource_manager_clone,
            global_dl_bucket: global_dl_bucket_clone,
            global_ul_bucket: global_ul_bucket_clone,
            file_priorities: file_priorities.clone(),
        };

        match TorrentManager::from_magnet(torrent_params, magnet, &magnet_link) {
            Ok(torrent_manager) => {
                tokio::spawn(async move {
                    let _ = torrent_manager
                        .run(torrent_control_state == TorrentControlState::Paused)
                        .await;
                });
            }
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "Failed to create new torrent manager from magnet: {:?}",
                    e
                );
                self.app_state.torrents.remove(&info_hash);
                self.app_state
                    .torrent_list_order
                    .retain(|ih| *ih != info_hash);
                self.torrent_metric_watch_rxs.remove(&info_hash);
                self.refresh_rss_derived();
            }
        }
    }

    async fn process_pending_commands(&mut self) {
        let commands = watcher::scan_watch_folders(&self.client_configs);
        for cmd in commands {
            let _ = self.app_command_tx.send(cmd).await;
        }
    }

    async fn rebind_listener(&mut self, new_port: u16) {
        match tokio::net::TcpListener::bind(format!("0.0.0.0:{}", new_port)).await {
            Ok(new_listener) => {
                self.listener = new_listener;
                // Note: client_configs.client_port is likely already updated by the caller (UpdateConfig)
                // but we ensure consistency here just in case.
                self.client_configs.client_port = new_port;

                tracing_event!(
                    Level::INFO,
                    "Successfully rebound listener to port {}",
                    new_port
                );

                // Notify all running managers of the new port
                for manager_tx in self.torrent_manager_command_txs.values() {
                    let _ = manager_tx.try_send(ManagerCommand::UpdateListenPort(new_port));
                }

                // Re-initialize DHT if enabled (Logic copied from handle_port_change)
                #[cfg(feature = "dht")]
                {
                    let bootstrap_nodes: Vec<&str> = self
                        .client_configs
                        .bootstrap_nodes
                        .iter()
                        .map(AsRef::as_ref)
                        .collect();

                    match Dht::builder()
                        .bootstrap(&bootstrap_nodes)
                        .port(new_port)
                        .server_mode()
                        .build()
                    {
                        Ok(new_dht_server) => {
                            let new_dht_handle = new_dht_server.as_async();
                            self.distributed_hash_table = new_dht_handle.clone();

                            for manager_tx in self.torrent_manager_command_txs.values() {
                                let _ = manager_tx.try_send(ManagerCommand::UpdateDhtHandle(
                                    new_dht_handle.clone(),
                                ));
                            }
                            self.dht_bootstrap_warning = None;
                            self.refresh_system_warning();
                        }
                        Err(e) => {
                            self.dht_bootstrap_warning = Some(format!(
                                "Warning: DHT bootstrap unavailable ({}). Running without bootstrap; retrying automatically.",
                                e
                            ));
                            self.refresh_system_warning();
                            tracing_event!(
                                Level::ERROR,
                                "Failed to rebuild DHT on new port: {}",
                                e
                            );
                        }
                    }
                }
            }
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "Failed to bind to new port {}: {}. Listener not updated.",
                    new_port,
                    e
                );
            }
        }
    }

    async fn download_rss_preview_item(&mut self, item: RssPreviewItem) {
        let Some(link) = item.link.clone() else {
            tracing_event!(
                Level::INFO,
                "Skipping RSS manual download: item has no link"
            );
            return;
        };

        let (added, info_hash) = if link.starts_with("magnet:") {
            let added = rss_ingest::write_magnet(&self.client_configs, link.as_str())
                .await
                .is_ok();
            let (v1_hash, v2_hash) = parse_hybrid_hashes(link.as_str());
            (added, v1_hash.or(v2_hash))
        } else if link.starts_with("http://") || link.starts_with("https://") {
            self.download_rss_torrent_from_url(link.as_str()).await
        } else {
            tracing_event!(
                Level::INFO,
                "Skipping RSS manual download: unsupported link scheme '{}'",
                link
            );
            (false, None)
        };

        if !added {
            return;
        }

        for preview in &mut self.app_state.rss_runtime.preview_items {
            if preview.dedupe_key == item.dedupe_key {
                preview.is_downloaded = true;
            }
        }

        let entry = RssHistoryEntry {
            dedupe_key: item.dedupe_key.clone(),
            info_hash: info_hash.map(hex::encode),
            guid: item.guid.clone(),
            link: item.link.clone(),
            title: item.title.clone(),
            source: item.source.clone(),
            date_iso: item
                .date_iso
                .clone()
                .unwrap_or_else(|| chrono::Utc::now().to_rfc3339()),
            added_via: crate::config::RssAddedVia::Manual,
        };
        let existing_idx = self
            .app_state
            .rss_runtime
            .history
            .iter()
            .position(|existing| existing.dedupe_key == entry.dedupe_key);
        if let Some(idx) = existing_idx {
            if self.app_state.rss_runtime.history[idx].info_hash.is_none()
                && entry.info_hash.is_some()
            {
                self.app_state.rss_runtime.history[idx].info_hash = entry.info_hash.clone();
                self.save_state_to_disk();
            }
        } else {
            self.app_state.rss_runtime.history.push(entry);
            self.save_state_to_disk();
        }

        if let Some(history_entry) = self
            .app_state
            .rss_runtime
            .history
            .iter()
            .find(|h| h.dedupe_key == item.dedupe_key)
            .cloned()
        {
            let _ = self.rss_downloaded_entry_tx.try_send(history_entry);
        }

        self.refresh_rss_derived();
    }

    async fn download_rss_torrent_from_url(&mut self, url: &str) -> (bool, Option<Vec<u8>>) {
        if !is_safe_rss_item_url(url).await {
            tracing_event!(
                Level::WARN,
                "RSS manual download blocked URL by network safety policy: {}",
                url
            );
            return (false, None);
        }

        let client = match reqwest::Client::builder()
            .user_agent("superseedr (https://github.com/Jagalite/superseedr)")
            .timeout(Duration::from_secs(RSS_MANUAL_DOWNLOAD_TIMEOUT_SECS))
            .build()
        {
            Ok(c) => c,
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "RSS manual download failed to build HTTP client: {}",
                    e
                );
                return (false, None);
            }
        };

        let response = match client.get(url).send().await {
            Ok(resp) => resp,
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "RSS manual download request failed for {}: {}",
                    url,
                    e
                );
                return (false, None);
            }
        };
        if !response.status().is_success() {
            tracing_event!(
                Level::ERROR,
                "RSS manual download HTTP status {} for {}",
                response.status(),
                url
            );
            return (false, None);
        }

        let bytes = match response.bytes().await {
            Ok(b) => b,
            Err(e) => {
                tracing_event!(
                    Level::ERROR,
                    "RSS manual download body read failed for {}: {}",
                    url,
                    e
                );
                return (false, None);
            }
        };
        if bytes.len() > RSS_MAX_TORRENT_DOWNLOAD_BYTES {
            tracing_event!(
                Level::ERROR,
                "RSS manual download exceeded max size for {} ({} bytes)",
                url,
                bytes.len()
            );
            return (false, None);
        }
        let Some(info_hash) = info_hash_from_torrent_bytes(bytes.as_ref()) else {
            tracing_event!(
                Level::ERROR,
                "RSS manual download produced invalid torrent payload for {}",
                url
            );
            return (false, None);
        };

        let file_hash = hex::encode(sha1::Sha1::digest(url.as_bytes()));
        let temp_path = std::env::temp_dir().join(format!("superseedr-rss-{}.torrent", file_hash));
        if let Err(e) = fs::write(&temp_path, bytes.as_ref()) {
            tracing_event!(
                Level::ERROR,
                "RSS manual download failed to write temp file {:?}: {}",
                temp_path,
                e
            );
            return (false, None);
        }

        let existed_before = self.app_state.torrents.contains_key(&info_hash);
        self.add_torrent_from_file(
            temp_path.clone(),
            self.client_configs.default_download_folder.clone(),
            false,
            TorrentControlState::Running,
            HashMap::new(),
            None,
        )
        .await;
        let exists_after = self.app_state.torrents.contains_key(&info_hash);

        if let Err(e) = fs::remove_file(&temp_path) {
            tracing_event!(
                Level::DEBUG,
                "RSS manual download temp cleanup skipped for {:?}: {}",
                temp_path,
                e
            );
        }
        if existed_before || exists_after {
            (true, Some(info_hash))
        } else {
            tracing_event!(
                Level::ERROR,
                "RSS manual download did not register torrent {} after add_torrent_from_file",
                hex::encode(&info_hash)
            );
            (false, None)
        }
    }

    async fn fetch_latest_version() -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
        let client = reqwest::Client::builder()
            .user_agent("superseedr (https://github.com/Jagalite/superseedr)")
            .build()?;

        let url = "https://crates.io/api/v1/crates/superseedr";
        let resp: CratesResponse = client.get(url).send().await?.json().await?;

        Ok(resp.krate.max_version)
    }

    pub fn generate_output_state(&self) -> AppOutputState {
        let s = &self.app_state;
        let torrent_metrics = s
            .torrents
            .iter()
            .map(|(k, v)| (k.clone(), v.latest_state.clone()))
            .collect();

        AppOutputState {
            run_time: s.run_time,
            cpu_usage: s.cpu_usage,
            ram_usage_percent: s.ram_usage_percent,
            total_download_bps: s.avg_download_history.last().copied().unwrap_or(0),
            total_upload_bps: s.avg_upload_history.last().copied().unwrap_or(0),
            torrents: torrent_metrics,
            settings: self.client_configs.clone(),
        }
    }

    pub fn dump_status_to_file(&self) {
        status::dump(self.generate_output_state(), self.shutdown_tx.clone());
    }
}

fn persisted_validation_status_from_piece_completion(
    total_pieces: u32,
    completed_pieces: u32,
    previous_validation_status: bool,
) -> bool {
    // Metadata may not be available yet for magnet sessions; preserve prior validation
    // only for the unknown 0/0 snapshot.
    if total_pieces == 0 && completed_pieces == 0 {
        return previous_validation_status;
    }
    total_pieces > 0 && completed_pieces == total_pieces
}

fn activity_marks_torrent_complete(activity_message: &str) -> bool {
    activity_message.contains("Seeding") || activity_message.contains("Finished")
}

fn torrent_has_skipped_files(metrics: &TorrentMetrics) -> bool {
    metrics
        .file_priorities
        .values()
        .any(|p| matches!(p, FilePriority::Skip))
}

pub fn torrent_is_effectively_incomplete(metrics: &TorrentMetrics) -> bool {
    if activity_marks_torrent_complete(&metrics.activity_message) {
        return false;
    }
    if torrent_has_skipped_files(metrics) {
        return false;
    }
    metrics.number_of_pieces_total > 0
        && metrics.number_of_pieces_completed < metrics.number_of_pieces_total
}

pub fn torrent_completion_percent(metrics: &TorrentMetrics) -> f64 {
    if activity_marks_torrent_complete(&metrics.activity_message) {
        return 100.0;
    }
    if torrent_has_skipped_files(metrics) {
        return 100.0;
    }
    if metrics.number_of_pieces_total == 0 {
        return 0.0;
    }

    ((metrics.number_of_pieces_completed as f64 / metrics.number_of_pieces_total as f64) * 100.0)
        .min(100.0)
}

fn calculate_adaptive_limits(client_configs: &Settings) -> (CalculatedLimits, Option<String>) {
    let effective_limit;
    let mut system_warning = None;
    const RECOMMENDED_MINIMUM: usize = 1024;

    if let Some(override_val) = client_configs.resource_limit_override {
        effective_limit = override_val;
        if effective_limit < RECOMMENDED_MINIMUM {
            system_warning = Some(format!(
                "Warning: Resource limit is set to {}. Performance may be degraded. Consider increasing with 'ulimit -n 65536'.",
                effective_limit
            ));
        }
    } else {
        #[cfg(unix)]
        {
            if let Ok((soft_limit, _)) = Resource::NOFILE.get() {
                effective_limit = soft_limit as usize;
                if effective_limit < RECOMMENDED_MINIMUM {
                    system_warning = Some(format!(
                        "Warning: System file handle limit is {}. Consider increasing with 'ulimit -n 65536'.",
                        effective_limit
                    ));
                }
            } else {
                effective_limit = RECOMMENDED_MINIMUM;
            }
        }
        #[cfg(windows)]
        {
            effective_limit = 8192;
        }
        #[cfg(not(any(unix, windows)))]
        {
            effective_limit = RECOMMENDED_MINIMUM;
        }
    }

    if let Some(warning) = &system_warning {
        tracing_event!(Level::WARN, "{}", warning);
    }

    let available_budget_after_reservation = effective_limit.saturating_sub(FILE_HANDLE_MINIMUM);
    let safe_budget = available_budget_after_reservation as f64 * SAFE_BUDGET_PERCENTAGE;
    const PEER_PROPORTION: f64 = 0.70;
    const DISK_READ_PROPORTION: f64 = 0.15;
    const DISK_WRITE_PROPORTION: f64 = 0.15;

    let limits = CalculatedLimits {
        reserve_permits: 0,
        max_connected_peers: (safe_budget * PEER_PROPORTION).max(10.0) as usize,
        disk_read_permits: (safe_budget * DISK_READ_PROPORTION).max(4.0) as usize,
        disk_write_permits: (safe_budget * DISK_WRITE_PROPORTION).max(4.0) as usize,
    };

    (limits, system_warning)
}

fn compose_system_warning(
    base_warning: Option<&str>,
    dht_bootstrap_warning: Option<&str>,
) -> Option<String> {
    match (base_warning, dht_bootstrap_warning) {
        (Some(base), Some(dht)) => Some(format!("{} | {}", base, dht)),
        (Some(base), None) => Some(base.to_string()),
        (None, Some(dht)) => Some(dht.to_string()),
        (None, None) => None,
    }
}

pub fn decode_info_hash(hash_string: &str) -> Result<Vec<u8>, String> {
    // Try Hex Decoding (Handles standard V1 and Hex-encoded V2 Multihash)
    if let Ok(bytes) = hex::decode(hash_string) {
        // V1: 20 bytes (SHA-1)
        if bytes.len() == 20 {
            return Ok(bytes);
        }
        // V2: 34 bytes (Multihash: 2 byte prefix + 32 byte SHA-256)
        // Prefix 0x12 (SHA2-256) + 0x20 (32 bytes)
        if bytes.len() == 34 && bytes[0] == 0x12 && bytes[1] == 0x20 {
            // Return truncated 20 bytes for internal ID
            return Ok(bytes[2..22].to_vec());
        }
    }

    // Try Base32 Decoding (Handles Base32-encoded V1 and V2)
    if let Ok(bytes) = BASE32.decode(hash_string.to_uppercase().as_bytes()) {
        if bytes.len() == 20 {
            return Ok(bytes);
        }
        if bytes.len() == 34 && bytes[0] == 0x12 && bytes[1] == 0x20 {
            return Ok(bytes[2..22].to_vec());
        }
    }

    Err(format!("Invalid info_hash format/length: {}", hash_string))
}

pub fn parse_hybrid_hashes(magnet_link: &str) -> (Option<Vec<u8>>, Option<Vec<u8>>) {
    let query = magnet_link
        .split_once('?')
        .map(|(_, q)| q)
        .unwrap_or(magnet_link);
    let mut v1: Option<Vec<u8>> = None;
    let mut v2: Option<Vec<u8>> = None;

    for part in query.split('&') {
        let Some((key, value)) = part.split_once('=') else {
            continue;
        };
        if !key.eq_ignore_ascii_case("xt") {
            continue;
        }

        const BTIH_PREFIX: &str = "urn:btih:";
        const BTMH_PREFIX: &str = "urn:btmh:";
        if value.len() > BTIH_PREFIX.len()
            && value
                .get(..BTIH_PREFIX.len())
                .is_some_and(|p| p.eq_ignore_ascii_case(BTIH_PREFIX))
        {
            v1 = value
                .get(BTIH_PREFIX.len()..)
                .and_then(|h| decode_info_hash(h).ok());
        } else if value.len() > BTMH_PREFIX.len()
            && value
                .get(..BTMH_PREFIX.len())
                .is_some_and(|p| p.eq_ignore_ascii_case(BTMH_PREFIX))
        {
            v2 = value
                .get(BTMH_PREFIX.len()..)
                .and_then(|h| decode_info_hash(h).ok());
        }
    }
    (v1, v2)
}

pub fn info_hash_from_torrent_bytes(bytes: &[u8]) -> Option<Vec<u8>> {
    let torrent = from_bytes(bytes).ok()?;

    let hash = if torrent.info.meta_version == Some(2) {
        if !torrent.info.pieces.is_empty() {
            let mut hasher = sha1::Sha1::new();
            hasher.update(&torrent.info_dict_bencode);
            hasher.finalize().to_vec()
        } else {
            let mut hasher = Sha256::new();
            hasher.update(&torrent.info_dict_bencode);
            hasher.finalize()[0..20].to_vec()
        }
    } else {
        let mut hasher = sha1::Sha1::new();
        hasher.update(&torrent.info_dict_bencode);
        hasher.finalize().to_vec()
    };

    Some(hash)
}

fn resolve_magnet_torrent_name(
    requested_name: &str,
    magnet_link: &str,
    info_hash: &[u8],
) -> String {
    let is_placeholder = requested_name.trim().is_empty() || requested_name == "Fetching name...";
    if !is_placeholder {
        return requested_name.to_string();
    }

    extract_magnet_display_name(magnet_link)
        .unwrap_or_else(|| format!("Magnet {}", hex::encode(info_hash)))
}

fn extract_magnet_display_name(magnet_link: &str) -> Option<String> {
    for raw_part in magnet_link.split('&') {
        let part = raw_part.strip_prefix("magnet:?").unwrap_or(raw_part);
        let Some((key, value)) = part.split_once('=') else {
            continue;
        };
        if key.eq_ignore_ascii_case("dn") {
            let value_for_decode = value.replace('+', "%20");
            if let Ok(decoded) = urlencoding::decode(&value_for_decode) {
                let name = decoded.trim();
                if !name.is_empty() {
                    return Some(name.to_string());
                }
            }
        }
    }
    None
}

pub(crate) fn clamp_selected_indices_in_state(app_state: &mut AppState) {
    let torrent_count = app_state.torrent_list_order.len();

    if torrent_count == 0 {
        app_state.ui.selected_torrent_index = 0;
    } else if app_state.ui.selected_torrent_index >= torrent_count {
        app_state.ui.selected_torrent_index = torrent_count - 1;
    }

    let peer_count = app_state
        .torrent_list_order
        .get(app_state.ui.selected_torrent_index)
        .and_then(|info_hash| app_state.torrents.get(info_hash))
        .map_or(0, |torrent| torrent.latest_state.peers.len());

    if peer_count == 0 {
        app_state.ui.selected_peer_index = 0;
    } else if app_state.ui.selected_peer_index >= peer_count {
        app_state.ui.selected_peer_index = peer_count - 1;
    }
}

pub(crate) fn sort_and_filter_torrent_list_state(app_state: &mut AppState) {
    let torrents_map = &app_state.torrents;
    let (sort_by, sort_direction) = app_state.torrent_sort;
    let search_query = &app_state.ui.search_query;

    let matcher = fuzzy_matcher::skim::SkimMatcherV2::default();
    let mut torrent_list: Vec<Vec<u8>> = torrents_map.keys().cloned().collect();

    if !search_query.is_empty() {
        torrent_list.retain(|info_hash| {
            let torrent_name = torrents_map
                .get(info_hash)
                .map_or("", |t| &t.latest_state.torrent_name);
            matcher.fuzzy_match(torrent_name, search_query).is_some()
        });
    }

    torrent_list.sort_by(|a_info_hash, b_info_hash| {
        let Some(a_torrent) = torrents_map.get(a_info_hash) else {
            return std::cmp::Ordering::Equal;
        };
        let Some(b_torrent) = torrents_map.get(b_info_hash) else {
            return std::cmp::Ordering::Equal;
        };

        let ordering = match sort_by {
            TorrentSortColumn::Name => a_torrent
                .latest_state
                .torrent_name
                .cmp(&b_torrent.latest_state.torrent_name),
            TorrentSortColumn::Down => b_torrent
                .smoothed_download_speed_bps
                .cmp(&a_torrent.smoothed_download_speed_bps),
            TorrentSortColumn::Up => b_torrent
                .smoothed_upload_speed_bps
                .cmp(&a_torrent.smoothed_upload_speed_bps),
            TorrentSortColumn::Progress => {
                let calc_progress = |t: &TorrentDisplayState| -> f64 {
                    if t.latest_state.number_of_pieces_total == 0 {
                        0.0
                    } else {
                        t.latest_state.number_of_pieces_completed as f64
                            / t.latest_state.number_of_pieces_total as f64
                    }
                };

                let a_prog = calc_progress(a_torrent);
                let b_prog = calc_progress(b_torrent);
                a_prog.total_cmp(&b_prog)
            }
        };

        let default_direction = match sort_by {
            TorrentSortColumn::Name => SortDirection::Ascending,
            _ => SortDirection::Descending,
        };
        let primary_ordering = if sort_direction != default_direction {
            ordering.reverse()
        } else {
            ordering
        };

        primary_ordering.then_with(|| {
            let calculate_weighted_activity = |t: &TorrentDisplayState| -> u64 {
                let window = 60;
                let mut score = 0;
                let mut sum_vec = |history: &Vec<u64>| {
                    for (i, &count) in history.iter().rev().take(window).enumerate() {
                        if count > 0 {
                            let weight = if i < 5 { (5 - i) as u64 * 10 } else { 1 };
                            score += count * weight;
                        }
                    }
                };
                sum_vec(&t.peer_discovery_history);
                sum_vec(&t.peer_connection_history);
                sum_vec(&t.peer_disconnect_history);
                score
            };

            let a_activity = calculate_weighted_activity(a_torrent);
            let b_activity = calculate_weighted_activity(b_torrent);
            b_activity.cmp(&a_activity)
        })
    });

    app_state.torrent_list_order = torrent_list;
    clamp_selected_indices_in_state(app_state);
}

fn rss_settings_changed(old_settings: &Settings, new_settings: &Settings) -> bool {
    new_settings.rss != old_settings.rss
}

fn should_load_persisted_torrent(torrent_settings: &TorrentSettings) -> bool {
    torrent_settings.torrent_control_state != TorrentControlState::Deleting
}

fn build_persist_payload(
    client_configs: &mut Settings,
    app_state: &mut AppState,
) -> PersistPayload {
    client_configs.lifetime_downloaded =
        app_state.lifetime_downloaded_from_config + app_state.session_total_downloaded;
    client_configs.lifetime_uploaded =
        app_state.lifetime_uploaded_from_config + app_state.session_total_uploaded;

    client_configs.torrent_sort_column = app_state.torrent_sort.0;
    client_configs.torrent_sort_direction = app_state.torrent_sort.1;
    client_configs.peer_sort_column = app_state.peer_sort.0;
    client_configs.peer_sort_direction = app_state.peer_sort.1;
    let old_validation_statuses: HashMap<String, bool> = client_configs
        .torrents
        .iter()
        .map(|cfg| (cfg.torrent_or_magnet.clone(), cfg.validation_status))
        .collect();

    client_configs.torrents = app_state
        .torrents
        .values()
        .map(|torrent| {
            let torrent_state = &torrent.latest_state;
            let previous_validation_status = old_validation_statuses
                .get(&torrent_state.torrent_or_magnet)
                .copied()
                .unwrap_or(false);

            let final_validation_status = persisted_validation_status_from_piece_completion(
                torrent_state.number_of_pieces_total,
                torrent_state.number_of_pieces_completed,
                previous_validation_status,
            );

            TorrentSettings {
                torrent_or_magnet: torrent_state.torrent_or_magnet.clone(),
                name: torrent_state.torrent_name.clone(),
                validation_status: final_validation_status,
                download_path: torrent_state.download_path.clone(),
                container_name: torrent_state.container_name.clone(),
                torrent_control_state: torrent_state.torrent_control_state.clone(),
                file_priorities: torrent_state.file_priorities.clone(),
            }
        })
        .collect();

    const RSS_HISTORY_LIMIT: usize = 1000;
    if app_state.rss_runtime.history.len() > RSS_HISTORY_LIMIT {
        let overflow = app_state.rss_runtime.history.len() - RSS_HISTORY_LIMIT;
        app_state.rss_runtime.history.drain(0..overflow);
    }

    let rss_state = RssPersistedState {
        history: app_state.rss_runtime.history.clone(),
        last_sync_at: app_state.rss_runtime.last_sync_at.clone(),
        feed_errors: app_state.rss_runtime.feed_errors.clone(),
    };

    let network_history = if app_state.network_history_restore_pending {
        None
    } else {
        app_state.network_history_state.rollups = app_state.network_history_rollups.to_snapshot();
        app_state.network_history_state.updated_at_unix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        app_state.next_network_history_persist_request_id = app_state
            .next_network_history_persist_request_id
            .saturating_add(1);
        Some(NetworkHistoryPersistRequest {
            request_id: app_state.next_network_history_persist_request_id,
            state: app_state.network_history_state.clone(),
        })
    };

    PersistPayload {
        settings: client_configs.clone(),
        rss_state,
        network_history,
    }
}

fn apply_network_history_persist_result(app_state: &mut AppState, request_id: u64, success: bool) {
    if success && app_state.pending_network_history_persist_request_id == Some(request_id) {
        app_state.network_history_dirty = false;
        app_state.pending_network_history_persist_request_id = None;
    }
}

fn should_persist_network_history_on_interval(app_state: &AppState) -> bool {
    app_state.network_history_dirty
}

fn queue_persistence_payload(
    tx: Option<&watch::Sender<Option<PersistPayload>>>,
    payload: PersistPayload,
) -> Result<(), ()> {
    let Some(tx) = tx else {
        return Err(());
    };
    tx.send_replace(Some(payload));
    if tx.is_closed() {
        return Err(());
    }
    Ok(())
}

async fn flush_persistence_writer_parts(
    persistence_tx: &mut Option<watch::Sender<Option<PersistPayload>>>,
    persistence_task: &mut Option<tokio::task::JoinHandle<()>>,
) {
    *persistence_tx = None;
    if let Some(handle) = persistence_task.take() {
        if let Err(e) = handle.await {
            tracing_event!(Level::ERROR, "Error joining persistence task: {}", e);
        }
    }
}

fn prune_rss_feed_errors(
    feed_errors: &mut HashMap<String, FeedSyncError>,
    settings: &Settings,
) -> bool {
    let configured_feed_urls: std::collections::HashSet<&str> = settings
        .rss
        .feeds
        .iter()
        .map(|feed| feed.url.as_str())
        .collect();
    let before = feed_errors.len();
    feed_errors.retain(|feed_url, _| configured_feed_urls.contains(feed_url.as_str()));
    feed_errors.len() != before
}

#[cfg(test)]
mod tests {
    use super::{
        apply_network_history_persist_result, build_persist_payload,
        clamp_selected_indices_in_state, compose_system_warning, extract_magnet_display_name,
        flush_persistence_writer_parts, parse_hybrid_hashes,
        persisted_validation_status_from_piece_completion, prune_rss_feed_errors,
        queue_persistence_payload, resolve_magnet_torrent_name, rss_settings_changed,
        should_load_persisted_torrent, should_persist_network_history_on_interval,
        sort_and_filter_torrent_list_state, torrent_completion_percent,
        torrent_is_effectively_incomplete, App, AppMode, AppState, FilePriority, PeerInfo,
        PersistPayload, SelectedHeader, SortDirection, TorrentControlState, TorrentDisplayState,
        TorrentMetrics, TorrentSortColumn, UiState,
    };
    use crate::config::TorrentSettings;
    use std::collections::HashMap;
    use std::time::Duration;
    use tokio::time;

    fn mock_display(name: &str, peer_count: usize) -> TorrentDisplayState {
        let mut display = TorrentDisplayState::default();
        display.latest_state.torrent_name = name.to_string();
        display.latest_state.peers = (0..peer_count)
            .map(|i| PeerInfo {
                address: format!("127.0.0.1:{}", 6000 + i),
                ..Default::default()
            })
            .collect();
        display
    }

    #[test]
    fn persisted_validation_status_is_true_only_when_complete() {
        assert!(!persisted_validation_status_from_piece_completion(
            0, 0, false
        ));
        assert!(!persisted_validation_status_from_piece_completion(
            10, 9, false
        ));
        assert!(persisted_validation_status_from_piece_completion(
            10, 10, false
        ));
    }

    #[test]
    fn persisted_validation_status_downgrades_when_incomplete() {
        assert!(
            !persisted_validation_status_from_piece_completion(10, 8, true),
            "Validation status must not stay true once piece completion regresses"
        );
    }

    #[test]
    fn persisted_validation_status_preserves_prior_true_for_metadata_unavailable_snapshot() {
        assert!(
            persisted_validation_status_from_piece_completion(0, 0, true),
            "0/0 snapshot should preserve prior validated status (magnet metadata pending)"
        );
    }

    #[test]
    fn should_draw_every_frame_in_normal_mode() {
        assert!(App::should_draw_this_frame(&AppMode::Normal, false));
        assert!(App::should_draw_this_frame(&AppMode::Normal, true));
    }

    #[test]
    fn should_draw_every_frame_in_welcome_mode() {
        assert!(App::should_draw_this_frame(&AppMode::Welcome, false));
        assert!(App::should_draw_this_frame(&AppMode::Welcome, true));
    }

    #[test]
    fn should_only_draw_dirty_in_power_saving_mode() {
        assert!(!App::should_draw_this_frame(&AppMode::PowerSaving, false));
        assert!(App::should_draw_this_frame(&AppMode::PowerSaving, true));
    }

    #[test]
    fn completion_helper_marks_seeding_complete() {
        let mut metrics = TorrentMetrics {
            number_of_pieces_total: 100,
            number_of_pieces_completed: 0,
            ..Default::default()
        };
        metrics.activity_message = "Seeding".to_string();

        assert!(!torrent_is_effectively_incomplete(&metrics));
        assert_eq!(torrent_completion_percent(&metrics), 100.0);
    }

    #[test]
    fn completion_helper_marks_skipped_files_complete() {
        let metrics = TorrentMetrics {
            number_of_pieces_total: 8,
            number_of_pieces_completed: 2,
            file_priorities: HashMap::from([(0, FilePriority::Skip)]),
            ..Default::default()
        };

        assert!(!torrent_is_effectively_incomplete(&metrics));
        assert_eq!(torrent_completion_percent(&metrics), 100.0);
    }

    #[test]
    fn clamp_selected_indices_clamps_torrent_and_peer_to_bounds() {
        let mut app_state = AppState::default();
        let hash_a = b"hash_a".to_vec();
        let hash_b = b"hash_b".to_vec();
        app_state
            .torrents
            .insert(hash_a.clone(), mock_display("alpha", 0));
        app_state
            .torrents
            .insert(hash_b.clone(), mock_display("beta", 2));
        app_state.torrent_list_order = vec![hash_a, hash_b];
        app_state.ui.selected_torrent_index = 99;
        app_state.ui.selected_peer_index = 99;

        clamp_selected_indices_in_state(&mut app_state);

        assert_eq!(app_state.ui.selected_torrent_index, 1);
        assert_eq!(app_state.ui.selected_peer_index, 1);
    }

    #[test]
    fn sort_and_filter_applies_query_and_clamps_selection() {
        let mut app_state = AppState {
            torrent_sort: (TorrentSortColumn::Name, SortDirection::Ascending),
            ui: UiState {
                selected_header: SelectedHeader::Torrent(0),
                selected_torrent_index: 5,
                search_query: "spha".to_string(),
                ..Default::default()
            },
            ..Default::default()
        };

        let hash_a = b"hash_a".to_vec();
        let hash_b = b"hash_b".to_vec();
        app_state
            .torrents
            .insert(hash_a.clone(), mock_display("samplealpha-24.04.iso", 0));
        app_state
            .torrents
            .insert(hash_b.clone(), mock_display("samplelinux.iso", 0));

        sort_and_filter_torrent_list_state(&mut app_state);

        assert_eq!(app_state.torrent_list_order, vec![hash_a]);
        assert_eq!(app_state.ui.selected_torrent_index, 0);
    }

    #[test]
    fn extract_magnet_display_name_decodes_dn() {
        let magnet =
            "magnet:?xt=urn:btih:1111111111111111111111111111111111111111&dn=SampleAlpha+24.04+ISO";
        assert_eq!(
            extract_magnet_display_name(magnet),
            Some("SampleAlpha 24.04 ISO".to_string())
        );
    }

    #[test]
    fn resolve_magnet_name_uses_dn_for_placeholder() {
        let info_hash = vec![0x11; 20];
        let magnet = "magnet:?xt=urn:btih:1111111111111111111111111111111111111111&dn=SampleBeta";
        assert_eq!(
            resolve_magnet_torrent_name("Fetching name...", magnet, &info_hash),
            "SampleBeta".to_string()
        );
    }

    #[test]
    fn resolve_magnet_name_falls_back_to_hash_label_when_dn_missing() {
        let info_hash = vec![0x22; 20];
        let magnet = "magnet:?xt=urn:btih:2222222222222222222222222222222222222222";
        assert_eq!(
            resolve_magnet_torrent_name("Fetching name...", magnet, &info_hash),
            format!("Magnet {}", hex::encode(&info_hash))
        );
    }

    #[test]
    fn extract_magnet_display_name_skips_malformed_segments() {
        let magnet = "magnet:?xt=urn:btih:1111111111111111111111111111111111111111&badsegment&dn=SampleGamma+Netinst";
        assert_eq!(
            extract_magnet_display_name(magnet),
            Some("SampleGamma Netinst".to_string())
        );
    }

    #[test]
    fn parse_hybrid_hashes_handles_case_insensitive_xt_and_urn_prefixes() {
        let magnet = "magnet:?XT=URN:BTIH:1111111111111111111111111111111111111111&xT=urn:BTMH:12201111111111111111111111111111111111111111111111111111111111111111";
        let (v1, v2) = parse_hybrid_hashes(magnet);
        assert_eq!(v1, Some(vec![0x11; 20]));
        assert_eq!(v2, Some(vec![0x11; 20]));
    }

    #[test]
    fn rss_settings_changed_detects_filter_updates() {
        let old = crate::config::Settings::default();
        let mut new = old.clone();
        new.rss.filters.push(crate::config::RssFilter {
            query: "samplealpha".to_string(),
            mode: crate::config::RssFilterMode::Fuzzy,
            enabled: true,
        });

        assert!(rss_settings_changed(&old, &new));
    }

    #[test]
    fn rss_settings_changed_ignores_non_rss_updates() {
        let old = crate::config::Settings::default();
        let mut new = old.clone();
        new.global_download_limit_bps += 1;

        assert!(!rss_settings_changed(&old, &new));
    }

    #[test]
    fn prune_rss_feed_errors_removes_deleted_feed_urls() {
        let mut settings = crate::config::Settings::default();
        settings.rss.feeds.push(crate::config::RssFeed {
            url: "https://active.example/rss.xml".to_string(),
            enabled: true,
        });

        let mut feed_errors = HashMap::new();
        feed_errors.insert(
            "https://active.example/rss.xml".to_string(),
            crate::config::FeedSyncError {
                message: "timeout".to_string(),
                occurred_at_iso: "2026-02-18T10:00:00Z".to_string(),
            },
        );
        feed_errors.insert(
            "https://removed.example/rss.xml".to_string(),
            crate::config::FeedSyncError {
                message: "403".to_string(),
                occurred_at_iso: "2026-02-18T10:01:00Z".to_string(),
            },
        );

        let changed = prune_rss_feed_errors(&mut feed_errors, &settings);
        assert!(changed);
        assert_eq!(feed_errors.len(), 1);
        assert!(feed_errors.contains_key("https://active.example/rss.xml"));
    }

    #[test]
    fn prune_rss_feed_errors_is_noop_when_all_urls_still_configured() {
        let mut settings = crate::config::Settings::default();
        settings.rss.feeds.push(crate::config::RssFeed {
            url: "https://active.example/rss.xml".to_string(),
            enabled: true,
        });

        let mut feed_errors = HashMap::new();
        feed_errors.insert(
            "https://active.example/rss.xml".to_string(),
            crate::config::FeedSyncError {
                message: "timeout".to_string(),
                occurred_at_iso: "2026-02-18T10:00:00Z".to_string(),
            },
        );

        let changed = prune_rss_feed_errors(&mut feed_errors, &settings);
        assert!(!changed);
        assert_eq!(feed_errors.len(), 1);
    }

    #[test]
    fn compose_system_warning_merges_base_and_dht_messages() {
        let composed = compose_system_warning(Some("base warning"), Some("dht warning"));
        assert_eq!(composed, Some("base warning | dht warning".to_string()));
    }

    #[test]
    fn compose_system_warning_handles_single_or_no_messages() {
        assert_eq!(
            compose_system_warning(Some("base warning"), None),
            Some("base warning".to_string())
        );
        assert_eq!(
            compose_system_warning(None, Some("dht warning")),
            Some("dht warning".to_string())
        );
        assert_eq!(compose_system_warning(None, None), None);
    }

    #[test]
    fn should_load_persisted_torrent_skips_only_deleting_entries() {
        let running = TorrentSettings {
            torrent_control_state: TorrentControlState::Running,
            ..Default::default()
        };
        let paused = TorrentSettings {
            torrent_control_state: TorrentControlState::Paused,
            ..Default::default()
        };
        let deleting = TorrentSettings {
            torrent_control_state: TorrentControlState::Deleting,
            ..Default::default()
        };

        assert!(should_load_persisted_torrent(&running));
        assert!(should_load_persisted_torrent(&paused));
        assert!(!should_load_persisted_torrent(&deleting));
    }

    #[tokio::test]
    async fn reset_tuning_for_objective_change_reschedules_deadline() {
        let settings = crate::config::Settings {
            client_port: 0,
            ..Default::default()
        };
        let mut app = App::new(settings).await.expect("build app");
        app.tuning_controller.on_second_tick();
        app.app_state.tuning_countdown = app.tuning_controller.countdown_secs();
        let stale_deadline = time::Instant::now() + Duration::from_secs(300);
        app.next_tuning_at = stale_deadline;

        app.reset_tuning_for_objective_change();

        let reset_cadence = app.tuning_controller.cadence_secs();
        let remaining = app
            .next_tuning_at
            .saturating_duration_since(time::Instant::now());

        assert_eq!(app.app_state.tuning_countdown, reset_cadence);
        assert!(app.next_tuning_at < stale_deadline);
        assert!(remaining <= Duration::from_secs(reset_cadence));

        let _ = app.shutdown_tx.send(());
    }

    #[test]
    fn network_history_interval_persistence_only_when_dirty() {
        let mut app_state = AppState {
            network_history_dirty: false,
            ..Default::default()
        };
        assert!(!should_persist_network_history_on_interval(&app_state));

        app_state.network_history_dirty = true;
        assert!(should_persist_network_history_on_interval(&app_state));
    }

    #[test]
    fn build_persist_payload_skips_network_history_while_restore_is_pending() {
        let mut settings = crate::config::Settings::default();
        let mut app_state = AppState {
            network_history_restore_pending: true,
            ..Default::default()
        };
        app_state.network_history_state.tiers.second_1s.push(
            crate::persistence::network_history::NetworkHistoryPoint {
                ts_unix: 41,
                download_bps: 1000,
                upload_bps: 100,
                backoff_ms_max: 0,
            },
        );

        let payload = build_persist_payload(&mut settings, &mut app_state);

        assert!(payload.network_history.is_none());
        assert_eq!(app_state.network_history_state.updated_at_unix, 0);
        assert_eq!(app_state.next_network_history_persist_request_id, 0);
    }

    #[test]
    fn build_persist_payload_syncs_rollup_snapshot_into_network_history_state() {
        let mut settings = crate::config::Settings::default();
        let snapshot = crate::persistence::network_history::NetworkHistoryRollupSnapshot {
            second_to_minute: crate::persistence::network_history::PersistedRollupAccumulator {
                count: 7,
                dl_sum: 7_000,
                ul_sum: 700,
                backoff_max: 9,
            },
            ..Default::default()
        };
        let mut app_state = AppState {
            network_history_rollups:
                crate::persistence::network_history::NetworkHistoryRollupState::from_snapshot(
                    &snapshot,
                ),
            ..Default::default()
        };

        let payload = build_persist_payload(&mut settings, &mut app_state);
        let network_history = payload
            .network_history
            .expect("network history payload should be present");

        assert_eq!(network_history.state.rollups, snapshot);
        assert_eq!(app_state.network_history_state.rollups, snapshot);
    }

    #[test]
    fn apply_network_history_persist_result_clears_dirty_only_for_latest_success() {
        let mut app_state = AppState {
            network_history_dirty: true,
            pending_network_history_persist_request_id: Some(2),
            ..Default::default()
        };

        apply_network_history_persist_result(&mut app_state, 1, true);
        assert!(app_state.network_history_dirty);
        assert_eq!(
            app_state.pending_network_history_persist_request_id,
            Some(2)
        );

        apply_network_history_persist_result(&mut app_state, 2, false);
        assert!(app_state.network_history_dirty);
        assert_eq!(
            app_state.pending_network_history_persist_request_id,
            Some(2)
        );

        apply_network_history_persist_result(&mut app_state, 2, true);
        assert!(!app_state.network_history_dirty);
        assert_eq!(app_state.pending_network_history_persist_request_id, None);
    }

    #[tokio::test]
    async fn queue_persistence_payload_carries_network_history_state() {
        let (tx, mut rx) = tokio::sync::watch::channel::<Option<PersistPayload>>(None);
        let mut network_history_state =
            crate::persistence::network_history::NetworkHistoryPersistedState {
                updated_at_unix: 42,
                ..Default::default()
            };
        network_history_state.tiers.second_1s.push(
            crate::persistence::network_history::NetworkHistoryPoint {
                ts_unix: 41,
                download_bps: 1000,
                upload_bps: 100,
                backoff_ms_max: 0,
            },
        );

        let payload = PersistPayload {
            settings: crate::config::Settings::default(),
            rss_state: crate::persistence::rss::RssPersistedState::default(),
            network_history: Some(super::NetworkHistoryPersistRequest {
                request_id: 7,
                state: network_history_state.clone(),
            }),
        };

        assert!(queue_persistence_payload(Some(&tx), payload).is_ok());
        assert!(rx.changed().await.is_ok());

        let received = rx.borrow().clone().expect("payload should be present");
        let network_history = received
            .network_history
            .expect("network history payload should be present");
        assert_eq!(network_history.request_id, 7);
        assert_eq!(
            network_history.state.updated_at_unix,
            network_history_state.updated_at_unix
        );
        assert_eq!(
            network_history.state.tiers.second_1s,
            network_history_state.tiers.second_1s
        );
    }

    #[tokio::test]
    async fn flush_persistence_writer_parts_drops_sender_and_joins_task() {
        let (tx, mut rx) = tokio::sync::watch::channel::<Option<PersistPayload>>(None);
        let task = tokio::spawn(async move { while rx.changed().await.is_ok() {} });

        let mut tx_opt = Some(tx);
        let mut task_opt = Some(task);
        flush_persistence_writer_parts(&mut tx_opt, &mut task_opt).await;

        assert!(tx_opt.is_none());
        assert!(task_opt.is_none());
    }
}
