use crate::common::interfaces::InterfaceSelection;
use crate::common::stats::{IfStats, PVEvent, TrackedConnectionEntry};
use chrono::{DateTime, Local};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::UNIX_EPOCH;

use super::options::{options_for_page, OptionId, OptionItem, OptionKind};
use super::utils;

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Page {
    Dashboard,
    Connections,
    Decoded,
    Diagnostics,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ConnectionsView {
    Summary,
    Detail,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ConnectionsPanelFocus {
    LiveList,
    Detail,
    StaleList,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DecodedPanelFocus {
    Table,
    Detail,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ConnectionsSortMode {
    Recent,
    Rate,
    Endpoint,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DecodedSortMode {
    NewestFirst,
    RequestType,
    Endpoint,
}

impl DecodedSortMode {
    pub fn next(self) -> Self {
        match self {
            Self::NewestFirst => Self::RequestType,
            Self::RequestType => Self::Endpoint,
            Self::Endpoint => Self::NewestFirst,
        }
    }

    pub fn label(self) -> &'static str {
        match self {
            Self::NewestFirst => "Newest",
            Self::RequestType => "Req",
            Self::Endpoint => "Endpoint",
        }
    }
}

impl ConnectionsSortMode {
    pub fn next(self) -> Self {
        match self {
            Self::Recent => Self::Rate,
            Self::Rate => Self::Endpoint,
            Self::Endpoint => Self::Recent,
        }
    }

    pub fn label(self) -> &'static str {
        match self {
            Self::Recent => "Recent",
            Self::Rate => "Rate",
            Self::Endpoint => "Endpoint",
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FilterField {
    RequestType,
    PvName,
    SourceIp,
    DestinationIp,
    Text,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FocusArea {
    Main,
    Options,
}

pub struct ConnectionsPartitions {
    pub live: Vec<TrackedConnectionEntry>,
    pub stale: Vec<TrackedConnectionEntry>,
    pub filtered_out_total: usize,
    pub source_total: usize,
}

#[derive(Clone, Debug)]
pub struct DecodedRow {
    pub id: String,
    pub ts: String,
    pub ts_nanos: u128,
    pub request_type: String,
    pub source: String,
    pub destination: String,
    pub context_preview: String,
    pub context_full: String,
    pub host_name: String,
}

pub struct AppState {
    pub data1: Vec<f64>,
    pub data2: Vec<f64>,
    pub timestamps: Vec<f64>,
    pub log_hist: Vec<String>,
    pub if_stats_local: IfStats,
    pub event_filter: HashMap<String, bool>,
    pub event_filter_keys: Vec<String>,
    pub pause: bool,
    pub ui_tick_rate_ms: u64,
    pub ui_last_tick_lag_ms: u64,
    pub ui_is_lagging: bool,
    pub show_host: bool,
    pub show_help: bool,
    pub current_page: Page,
    pub focus_area: FocusArea,
    pub dashboard_options_index: usize,
    pub connections_options_index: usize,
    pub decoded_options_index: usize,
    pub diagnostics_options_index: usize,
    pub connections_view: ConnectionsView,
    pub connections_panel_focus: ConnectionsPanelFocus,
    pub decoded_panel_focus: DecodedPanelFocus,
    pub decoded_filter_field: FilterField,
    pub decoded_edit_mode: bool,
    pub decoded_filter_request_type: String,
    pub decoded_filter_pv: String,
    pub decoded_filter_source_ip: String,
    pub decoded_filter_destination_ip: String,
    pub decoded_filter_text: String,
    pub decoded_show_endpoints: bool,
    pub show_midstream: bool,
    pub filter_broadcast_beacon: bool,
    pub connections_search_text: String,
    pub connections_search_edit: bool,
    pub dashboard_top_n: usize,
    pub connections_live_scroll: u16,
    pub connections_stale_scroll: u16,
    pub decoded_table_scroll: u16,
    pub decoded_detail_scroll: u16,
    pub decoded_table_view_height: u16,
    pub decoded_selected_index: i32,
    pub decoded_selected_id: Option<String>,
    pub decoded_sort_mode: DecodedSortMode,
    pub connections_detail_scroll: u16,
    pub connections_live_list_view_height: u16,
    pub connections_stale_list_view_height: u16,
    pub connections_live_selected_index: i32,
    pub connections_stale_selected_index: i32,
    pub connections_selected_id: Option<String>,
    pub connections_live_window_secs: u64,
    pub connections_show_stale_panel: bool,
    pub connections_sort_mode: ConnectionsSortMode,
    pub interface_selection: Option<Arc<InterfaceSelection>>,
    pub interface_menu_open: bool,
    pub interface_menu_index: usize,
    pub last_interface_index: Option<usize>,
}

impl AppState {
    pub fn new() -> Self {
        let data1 = vec![0.; 200];
        let data2 = vec![0.; 200];
        let timestamps = vec![0.; 200];
        let log_hist = vec!["".to_string(); 40];
        let if_stats_local = IfStats::new();
        let mut event_filter = HashMap::new();
        for req in utils::REQUEST_TYPES {
            event_filter.insert((*req).to_string(), true);
        }

        let mut event_filter_keys = event_filter.keys().cloned().collect::<Vec<String>>();
        event_filter_keys.sort();

        Self {
            data1,
            data2,
            timestamps,
            log_hist,
            if_stats_local,
            event_filter,
            event_filter_keys,
            pause: false,
            ui_tick_rate_ms: 100,
            ui_last_tick_lag_ms: 0,
            ui_is_lagging: false,
            show_host: false,
            show_help: false,
            current_page: Page::Dashboard,
            focus_area: FocusArea::Options,
            dashboard_options_index: 0,
            connections_options_index: 0,
            decoded_options_index: 0,
            diagnostics_options_index: 0,
            connections_view: ConnectionsView::Summary,
            connections_panel_focus: ConnectionsPanelFocus::LiveList,
            decoded_panel_focus: DecodedPanelFocus::Table,
            decoded_filter_field: FilterField::RequestType,
            decoded_edit_mode: false,
            decoded_filter_request_type: String::new(),
            decoded_filter_pv: String::new(),
            decoded_filter_source_ip: String::new(),
            decoded_filter_destination_ip: String::new(),
            decoded_filter_text: String::new(),
            decoded_show_endpoints: true,
            show_midstream: true,
            filter_broadcast_beacon: true,
            connections_search_text: String::new(),
            connections_search_edit: false,
            dashboard_top_n: 10,
            connections_live_scroll: 0,
            connections_stale_scroll: 0,
            decoded_table_scroll: 0,
            decoded_detail_scroll: 0,
            decoded_table_view_height: 0,
            decoded_selected_index: 0,
            decoded_selected_id: None,
            decoded_sort_mode: DecodedSortMode::NewestFirst,
            connections_detail_scroll: 0,
            connections_live_list_view_height: 0,
            connections_stale_list_view_height: 0,
            connections_live_selected_index: 0,
            connections_stale_selected_index: 0,
            connections_selected_id: None,
            connections_live_window_secs: 30,
            connections_show_stale_panel: false,
            connections_sort_mode: ConnectionsSortMode::Recent,
            interface_selection: None,
            interface_menu_open: false,
            interface_menu_index: 0,
            last_interface_index: None,
        }
    }

    pub fn set_interface_selection(&mut self, selection: Arc<InterfaceSelection>) {
        self.last_interface_index = Some(selection.current_index());
        self.interface_menu_index = selection.current_index();
        self.interface_selection = Some(selection);
    }

    pub fn interface_count(&self) -> usize {
        self.interface_selection
            .as_ref()
            .map(|selection| selection.len())
            .unwrap_or(0)
    }

    pub fn current_interface_index(&self) -> Option<usize> {
        self.interface_selection
            .as_ref()
            .map(|selection| selection.current_index())
    }

    pub fn current_interface_label(&self) -> Option<String> {
        let selection = self.interface_selection.as_ref()?;
        let idx = selection.current_index();
        let total = selection.len();
        let name = selection
            .interfaces
            .get(idx)
            .map(|iface| iface.name.clone())
            .unwrap_or_else(|| "unknown".to_string());
        Some(format!("IF {}/{} {}", idx + 1, total.max(1), name))
    }

    pub fn open_interface_menu(&mut self) {
        if self.interface_count() == 0 {
            return;
        }
        self.interface_menu_open = true;
        self.interface_menu_index = self.current_interface_index().unwrap_or(0);
    }

    pub fn close_interface_menu(&mut self) {
        self.interface_menu_open = false;
    }

    pub fn move_interface_menu_selection(&mut self, delta: i32) {
        let count = self.interface_count();
        if count == 0 {
            return;
        }
        let count_i32 = count as i32;
        let idx = self.interface_menu_index as i32;
        let next = (idx + delta).rem_euclid(count_i32);
        self.interface_menu_index = next as usize;
    }

    pub fn apply_interface_menu_selection(&mut self) {
        if let Some(selection) = self.interface_selection.as_ref() {
            selection.set_index(self.interface_menu_index);
        }
        self.close_interface_menu();
    }

    pub fn cycle_interface(&mut self) -> bool {
        if let Some(selection) = self.interface_selection.as_ref() {
            if selection.len() == 0 {
                return false;
            }
            selection.cycle_next();
            return true;
        }
        false
    }

    pub fn sync_interface_change(&mut self) -> bool {
        let current = self.current_interface_index();
        if current != self.last_interface_index {
            self.last_interface_index = current;
            return true;
        }
        false
    }

    pub fn reset_charts(&mut self) {
        self.data1 = vec![0.; 200];
        self.data2 = vec![0.; 200];
        self.timestamps = vec![0.; 200];
        self.log_hist = vec!["".to_string(); 40];
    }

    pub fn reset_selections(&mut self) {
        self.connections_selected_id = None;
        self.connections_live_selected_index = 0;
        self.connections_stale_selected_index = 0;
        self.connections_live_scroll = 0;
        self.connections_stale_scroll = 0;
        self.connections_detail_scroll = 0;
        self.decoded_selected_id = None;
        self.decoded_selected_index = 0;
        self.decoded_table_scroll = 0;
        self.decoded_detail_scroll = 0;
    }

    pub fn active_filter_mut(&mut self) -> &mut String {
        match self.decoded_filter_field {
            FilterField::RequestType => &mut self.decoded_filter_request_type,
            FilterField::PvName => &mut self.decoded_filter_pv,
            FilterField::SourceIp => &mut self.decoded_filter_source_ip,
            FilterField::DestinationIp => &mut self.decoded_filter_destination_ip,
            FilterField::Text => &mut self.decoded_filter_text,
        }
    }

    pub fn clear_filters(&mut self) {
        self.decoded_filter_request_type.clear();
        self.decoded_filter_pv.clear();
        self.decoded_filter_source_ip.clear();
        self.decoded_filter_destination_ip.clear();
        self.decoded_filter_text.clear();
    }

    pub fn clear_connections_search(&mut self) {
        self.connections_search_text.clear();
    }

    pub fn cycle_connections_sort_mode(&mut self) {
        self.connections_sort_mode = self.connections_sort_mode.next();
    }

    pub fn cycle_connections_live_window(&mut self) {
        const WINDOW_CHOICES: [u64; 4] = [10, 30, 60, 120];
        let next_idx = WINDOW_CHOICES
            .iter()
            .position(|v| *v == self.connections_live_window_secs)
            .map(|idx| (idx + 1) % WINDOW_CHOICES.len())
            .unwrap_or(1);
        self.connections_live_window_secs = WINDOW_CHOICES[next_idx];
    }

    pub fn cycle_decoded_sort_mode(&mut self) {
        self.decoded_sort_mode = self.decoded_sort_mode.next();
    }

    pub fn current_options_index_mut(&mut self) -> &mut usize {
        match self.current_page {
            Page::Dashboard => &mut self.dashboard_options_index,
            Page::Connections => &mut self.connections_options_index,
            Page::Decoded => &mut self.decoded_options_index,
            Page::Diagnostics => &mut self.diagnostics_options_index,
        }
    }

    pub fn current_options_index(&self) -> usize {
        match self.current_page {
            Page::Dashboard => self.dashboard_options_index,
            Page::Connections => self.connections_options_index,
            Page::Decoded => self.decoded_options_index,
            Page::Diagnostics => self.diagnostics_options_index,
        }
    }

    pub fn current_options(&self) -> Vec<OptionItem> {
        options_for_page(self.current_page, self.event_filter_keys.len())
    }

    pub fn normalize_options_index(&mut self) {
        let items = self.current_options();
        if items.is_empty() {
            return;
        }
        let len = items.len();
        let mut idx = self.current_options_index().min(len - 1);
        if items[idx].kind == OptionKind::Section {
            for offset in 1..=len {
                let next = (idx + offset) % len;
                if items[next].kind != OptionKind::Section {
                    idx = next;
                    break;
                }
            }
        }
        *self.current_options_index_mut() = idx;
        if let OptionId::DecFilter(field) = items[idx].id {
            self.decoded_filter_field = field;
        }
    }

    pub fn move_options_selection(&mut self, delta: i32) {
        let items = self.current_options();
        if items.is_empty() {
            return;
        }
        let len = items.len() as i32;
        let mut idx = self.current_options_index() as i32;
        let start = idx;
        loop {
            idx = (idx + delta).rem_euclid(len);
            if items[idx as usize].kind != OptionKind::Section {
                break;
            }
            if idx == start {
                break;
            }
        }
        *self.current_options_index_mut() = idx as usize;
        if let OptionId::DecFilter(field) = items[idx as usize].id {
            self.decoded_filter_field = field;
        }
    }

    pub fn activate_option(&mut self, item: OptionItem) {
        match item.id {
            OptionId::Pause => {
                self.pause = !self.pause;
            }
            OptionId::ShowHost => {
                self.show_host = !self.show_host;
            }
            OptionId::ResetCharts => {
                self.data1 = vec![0.; 200];
                self.data2 = vec![0.; 200];
            }
            OptionId::EventFilter(idx) => {
                if let Some(key) = self.event_filter_keys.get(idx) {
                    if let Some(value) = self.event_filter.get_mut(key) {
                        *value = !*value;
                    }
                }
            }
            OptionId::ConnView => {
                self.connections_view = match self.connections_view {
                    ConnectionsView::Summary => ConnectionsView::Detail,
                    ConnectionsView::Detail => ConnectionsView::Summary,
                };
                self.connections_selected_id = None;
                self.connections_live_selected_index = 0;
                self.connections_stale_selected_index = 0;
                self.connections_live_scroll = 0;
                self.connections_stale_scroll = 0;
                self.connections_detail_scroll = 0;
            }
            OptionId::ConnMidstream => {
                self.show_midstream = !self.show_midstream;
            }
            OptionId::ConnBroadcast => {
                self.filter_broadcast_beacon = !self.filter_broadcast_beacon;
            }
            OptionId::ConnSort => {
                self.cycle_connections_sort_mode();
            }
            OptionId::ConnLiveWindow => {
                self.cycle_connections_live_window();
            }
            OptionId::ConnShowStale => {
                self.connections_show_stale_panel = !self.connections_show_stale_panel;
                if !self.connections_show_stale_panel
                    && self.connections_panel_focus == ConnectionsPanelFocus::StaleList
                {
                    self.connections_panel_focus = ConnectionsPanelFocus::LiveList;
                }
            }
            OptionId::ConnSearch => {
                self.connections_search_edit = true;
            }
            OptionId::ConnClearSearch => {
                self.clear_connections_search();
                self.connections_live_scroll = 0;
                self.connections_stale_scroll = 0;
                self.connections_detail_scroll = 0;
            }
            OptionId::DecFilter(field) => {
                self.decoded_filter_field = field;
                self.decoded_edit_mode = true;
            }
            OptionId::DecClearFilters => {
                self.clear_filters();
                self.decoded_table_scroll = 0;
                self.decoded_detail_scroll = 0;
            }
            OptionId::DecSort => {
                self.cycle_decoded_sort_mode();
            }
            OptionId::DecQuickFilter => {
                self.decoded_filter_field = FilterField::Text;
                self.decoded_edit_mode = true;
            }
            OptionId::DecShowEndpoints => {
                self.decoded_show_endpoints = !self.decoded_show_endpoints;
            }
        }
    }

    fn connections_source(&self) -> &[TrackedConnectionEntry] {
        match self.connections_view {
            ConnectionsView::Summary => &self.if_stats_local.tracked_connections_summary,
            ConnectionsView::Detail => &self.if_stats_local.tracked_connections_detail,
        }
    }

    fn entry_matches_search(entry: &TrackedConnectionEntry, needle: &str) -> bool {
        entry.endpoint_a.contains(needle)
            || entry.endpoint_b.contains(needle)
            || entry.id.contains(needle)
            || entry
                .pv_name
                .as_ref()
                .is_some_and(|pv_name| pv_name.contains(needle))
            || entry.recent_messages.iter().any(|m| m.contains(needle))
    }

    fn sort_entries(
        entries: &mut Vec<TrackedConnectionEntry>,
        mode: ConnectionsSortMode,
        is_detail: bool,
    ) {
        entries.sort_by(|a, b| match mode {
            ConnectionsSortMode::Recent => a
                .last_seen_secs
                .cmp(&b.last_seen_secs)
                .then_with(|| {
                    b.updates_per_sec
                        .partial_cmp(&a.updates_per_sec)
                        .unwrap_or(Ordering::Equal)
                })
                .then_with(|| a.id.cmp(&b.id)),
            ConnectionsSortMode::Rate => b
                .updates_per_sec
                .partial_cmp(&a.updates_per_sec)
                .unwrap_or(Ordering::Equal)
                .then_with(|| a.last_seen_secs.cmp(&b.last_seen_secs))
                .then_with(|| a.id.cmp(&b.id)),
            ConnectionsSortMode::Endpoint => {
                if is_detail {
                    a.endpoint_a
                        .cmp(&b.endpoint_a)
                        .then_with(|| a.endpoint_b.cmp(&b.endpoint_b))
                        .then_with(|| {
                            a.pv_name
                                .as_deref()
                                .unwrap_or("")
                                .cmp(b.pv_name.as_deref().unwrap_or(""))
                        })
                        .then_with(|| a.cid.cmp(&b.cid))
                        .then_with(|| a.sid.cmp(&b.sid))
                } else {
                    a.endpoint_a
                        .cmp(&b.endpoint_a)
                        .then_with(|| a.endpoint_b.cmp(&b.endpoint_b))
                        .then_with(|| a.id.cmp(&b.id))
                }
            }
        });
    }

    pub fn connections_partitions(&self) -> ConnectionsPartitions {
        let source = self.connections_source();
        let source_total = source.len();
        let mut filtered_out_total = 0;

        let mut visible = Vec::new();
        for entry in source {
            if !self.show_midstream && entry.mid_stream {
                filtered_out_total += 1;
                continue;
            }
            if self.filter_broadcast_beacon && (entry.is_beacon || entry.is_broadcast) {
                filtered_out_total += 1;
                continue;
            }
            if !self.connections_search_text.is_empty()
                && !Self::entry_matches_search(entry, &self.connections_search_text)
            {
                filtered_out_total += 1;
                continue;
            }
            visible.push(entry.clone());
        }

        let mut live: Vec<TrackedConnectionEntry> = Vec::new();
        let mut stale: Vec<TrackedConnectionEntry> = Vec::new();
        for entry in visible {
            if entry.last_seen_secs <= self.connections_live_window_secs {
                live.push(entry);
            } else {
                stale.push(entry);
            }
        }

        let is_detail = self.connections_view == ConnectionsView::Detail;
        Self::sort_entries(&mut live, self.connections_sort_mode, is_detail);
        Self::sort_entries(&mut stale, self.connections_sort_mode, is_detail);

        ConnectionsPartitions {
            live,
            stale,
            filtered_out_total,
            source_total,
        }
    }

    pub fn sync_connections_selection(
        &mut self,
        live: &[TrackedConnectionEntry],
        stale: &[TrackedConnectionEntry],
    ) {
        if !self.connections_show_stale_panel
            && self.connections_panel_focus == ConnectionsPanelFocus::StaleList
        {
            self.connections_panel_focus = ConnectionsPanelFocus::LiveList;
        }

        if live.is_empty() && stale.is_empty() {
            self.connections_selected_id = None;
            self.connections_live_selected_index = 0;
            self.connections_stale_selected_index = 0;
            self.connections_live_scroll = 0;
            self.connections_stale_scroll = 0;
            return;
        }

        let mut found = false;
        if let Some(selected_id) = self.connections_selected_id.as_ref() {
            if let Some((idx, _)) = live.iter().enumerate().find(|(_, e)| e.id == *selected_id) {
                self.connections_live_selected_index = idx as i32;
                found = true;
            }
            if let Some((idx, _)) = stale.iter().enumerate().find(|(_, e)| e.id == *selected_id) {
                self.connections_stale_selected_index = idx as i32;
                found = true;
            }
        }

        if found {
            return;
        }

        if self.connections_panel_focus == ConnectionsPanelFocus::StaleList
            && self.connections_show_stale_panel
            && !stale.is_empty()
        {
            let idx = self
                .connections_stale_selected_index
                .clamp(0, stale.len().saturating_sub(1) as i32) as usize;
            self.connections_selected_id = Some(stale[idx].id.clone());
            self.connections_stale_selected_index = idx as i32;
            return;
        }

        if !live.is_empty() {
            let idx = self
                .connections_live_selected_index
                .clamp(0, live.len().saturating_sub(1) as i32) as usize;
            self.connections_selected_id = Some(live[idx].id.clone());
            self.connections_live_selected_index = idx as i32;
            return;
        }

        if !stale.is_empty() {
            let idx = self
                .connections_stale_selected_index
                .clamp(0, stale.len().saturating_sub(1) as i32) as usize;
            self.connections_selected_id = Some(stale[idx].id.clone());
            self.connections_stale_selected_index = idx as i32;
        }
    }

    pub fn selected_connection<'a>(
        &self,
        live: &'a [TrackedConnectionEntry],
        stale: &'a [TrackedConnectionEntry],
    ) -> Option<&'a TrackedConnectionEntry> {
        if let Some(selected_id) = self.connections_selected_id.as_ref() {
            if let Some(entry) = live.iter().find(|entry| entry.id == *selected_id) {
                return Some(entry);
            }
            if let Some(entry) = stale.iter().find(|entry| entry.id == *selected_id) {
                return Some(entry);
            }
        }

        if self.connections_panel_focus == ConnectionsPanelFocus::StaleList
            && self.connections_show_stale_panel
        {
            let idx = self.connections_stale_selected_index.max(0) as usize;
            return stale.get(idx);
        }

        let idx = self.connections_live_selected_index.max(0) as usize;
        live.get(idx).or_else(|| stale.first())
    }

    pub fn set_selected_live(&mut self, idx: usize, live: &[TrackedConnectionEntry]) {
        if let Some(entry) = live.get(idx) {
            self.connections_live_selected_index = idx as i32;
            self.connections_selected_id = Some(entry.id.clone());
        }
    }

    pub fn set_selected_stale(&mut self, idx: usize, stale: &[TrackedConnectionEntry]) {
        if let Some(entry) = stale.get(idx) {
            self.connections_stale_selected_index = idx as i32;
            self.connections_selected_id = Some(entry.id.clone());
        }
    }

    pub fn top_n_counts(map: &HashMap<String, u64>, n: usize) -> Vec<(String, u64)> {
        let mut items = map
            .iter()
            .map(|(k, v)| (k.clone(), *v))
            .collect::<Vec<(String, u64)>>();
        items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
        items.truncate(n);
        items
    }

    fn event_ts_nanos(event: &PVEvent) -> u128 {
        event
            .system_time
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0)
    }

    fn event_ts_label(event: &PVEvent) -> String {
        let dt: DateTime<Local> = DateTime::from(event.system_time);
        dt.format("%H:%M:%S%.3f").to_string()
    }

    fn decoded_preview(context: &str, max_len: usize) -> String {
        let mut out = context.to_string();
        if out.len() > max_len {
            out.truncate(max_len.saturating_sub(3));
            out.push_str("...");
        }
        out
    }

    fn decoded_id(event: &PVEvent) -> String {
        format!(
            "{}|{}|{}|{}|{}",
            Self::event_ts_nanos(event),
            event.source_ip,
            event.destination_ip,
            event.request_type,
            event.context
        )
    }

    fn event_matches_decoded_filters(&self, event: &PVEvent) -> bool {
        if !*self.event_filter.get(&event.request_type).unwrap_or(&true) {
            return false;
        }
        if !self.decoded_filter_request_type.is_empty()
            && !event
                .request_type
                .contains(&self.decoded_filter_request_type)
        {
            return false;
        }
        if !self.decoded_filter_source_ip.is_empty()
            && !event.source_ip.contains(&self.decoded_filter_source_ip)
        {
            return false;
        }
        if !self.decoded_filter_destination_ip.is_empty()
            && !event
                .destination_ip
                .contains(&self.decoded_filter_destination_ip)
        {
            return false;
        }
        if !self.decoded_filter_pv.is_empty() && !event.context.contains(&self.decoded_filter_pv) {
            return false;
        }
        if !self.decoded_filter_text.is_empty() {
            let needle = self.decoded_filter_text.as_str();
            let text_match = event.request_type.contains(needle)
                || event.source_ip.contains(needle)
                || event.destination_ip.contains(needle)
                || event.context.contains(needle)
                || event.host_name.contains(needle);
            if !text_match {
                return false;
            }
        }
        true
    }

    pub fn decoded_rows(&self) -> Vec<DecodedRow> {
        let mut rows = self
            .if_stats_local
            .events
            .iter()
            .filter(|event| self.event_matches_decoded_filters(event))
            .map(|event| DecodedRow {
                id: Self::decoded_id(event),
                ts: Self::event_ts_label(event),
                ts_nanos: Self::event_ts_nanos(event),
                request_type: event.request_type.clone(),
                source: event.source_ip.clone(),
                destination: event.destination_ip.clone(),
                context_preview: Self::decoded_preview(&event.context, 72),
                context_full: event.context.clone(),
                host_name: event.host_name.clone(),
            })
            .collect::<Vec<DecodedRow>>();

        rows.sort_by(|a, b| match self.decoded_sort_mode {
            DecodedSortMode::NewestFirst => {
                b.ts_nanos.cmp(&a.ts_nanos).then_with(|| a.id.cmp(&b.id))
            }
            DecodedSortMode::RequestType => a
                .request_type
                .cmp(&b.request_type)
                .then_with(|| b.ts_nanos.cmp(&a.ts_nanos))
                .then_with(|| a.id.cmp(&b.id)),
            DecodedSortMode::Endpoint => a
                .source
                .cmp(&b.source)
                .then_with(|| a.destination.cmp(&b.destination))
                .then_with(|| b.ts_nanos.cmp(&a.ts_nanos))
                .then_with(|| a.id.cmp(&b.id)),
        });

        rows
    }

    pub fn sync_decoded_selection(&mut self, rows: &[DecodedRow]) {
        if rows.is_empty() {
            self.decoded_selected_index = 0;
            self.decoded_selected_id = None;
            self.decoded_table_scroll = 0;
            return;
        }

        if let Some(ref selected_id) = self.decoded_selected_id {
            if let Some((idx, _)) = rows
                .iter()
                .enumerate()
                .find(|(_, row)| &row.id == selected_id)
            {
                self.decoded_selected_index = idx as i32;
                return;
            }
        }

        let max_idx = rows.len().saturating_sub(1) as i32;
        if self.decoded_selected_index < 0 {
            self.decoded_selected_index = 0;
        } else if self.decoded_selected_index > max_idx {
            self.decoded_selected_index = max_idx;
        }
        let idx = self.decoded_selected_index as usize;
        self.decoded_selected_id = Some(rows[idx].id.clone());
    }

    pub fn selected_decoded_row<'a>(&self, rows: &'a [DecodedRow]) -> Option<&'a DecodedRow> {
        if let Some(ref selected_id) = self.decoded_selected_id {
            if let Some(row) = rows.iter().find(|row| &row.id == selected_id) {
                return Some(row);
            }
        }
        let idx = self.decoded_selected_index.max(0) as usize;
        rows.get(idx).or_else(|| rows.first())
    }

    pub fn set_selected_decoded(&mut self, idx: usize, rows: &[DecodedRow]) {
        if let Some(row) = rows.get(idx) {
            self.decoded_selected_index = idx as i32;
            self.decoded_selected_id = Some(row.id.clone());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{Duration, UNIX_EPOCH};

    fn mk_entry(
        id: &str,
        endpoint_a: &str,
        endpoint_b: &str,
        last_seen_secs: u64,
        updates_per_sec: f64,
    ) -> TrackedConnectionEntry {
        TrackedConnectionEntry {
            id: id.to_string(),
            endpoint_a: endpoint_a.to_string(),
            endpoint_b: endpoint_b.to_string(),
            cid: None,
            sid: None,
            pv_name: None,
            channel_count: 1,
            operation_count: 1,
            updates_per_sec,
            last_seen_secs,
            mid_stream: false,
            is_beacon: false,
            is_broadcast: false,
            recent_messages: vec![],
        }
    }

    fn mk_event(ts_secs: u64, req: &str, src: &str, dst: &str, context: &str) -> PVEvent {
        let mut event = PVEvent::new(src.to_string(), dst.to_string(), req.to_string());
        event.context = context.to_string();
        event.system_time = UNIX_EPOCH + Duration::from_secs(ts_secs);
        event
    }

    #[test]
    fn selection_remains_pinned_when_timestamps_change() {
        let mut state = AppState::new();
        state.if_stats_local.tracked_connections_summary = vec![
            mk_entry("a", "10.0.0.1:1111", "10.0.0.2:2222", 1, 2.0),
            mk_entry("b", "10.0.0.3:3333", "10.0.0.4:4444", 2, 3.0),
        ];
        state.connections_selected_id = Some("b".to_string());

        let p1 = state.connections_partitions();
        state.sync_connections_selection(&p1.live, &p1.stale);
        assert_eq!(state.connections_selected_id.as_deref(), Some("b"));

        state.if_stats_local.tracked_connections_summary[1].last_seen_secs = 6;
        let p2 = state.connections_partitions();
        state.sync_connections_selection(&p2.live, &p2.stale);
        assert_eq!(state.connections_selected_id.as_deref(), Some("b"));
    }

    #[test]
    fn recent_sort_and_rate_sort_are_deterministic() {
        let mut state = AppState::new();
        state.if_stats_local.tracked_connections_summary = vec![
            mk_entry("c", "10.0.0.9:1", "10.0.0.9:2", 4, 2.0),
            mk_entry("a", "10.0.0.1:1", "10.0.0.1:2", 1, 2.0),
            mk_entry("b", "10.0.0.2:1", "10.0.0.2:2", 1, 3.0),
        ];

        state.connections_sort_mode = ConnectionsSortMode::Recent;
        let recent = state.connections_partitions();
        let recent_ids = recent
            .live
            .iter()
            .map(|e| e.id.as_str())
            .collect::<Vec<_>>();
        assert_eq!(recent_ids, vec!["b", "a", "c"]);

        state.connections_sort_mode = ConnectionsSortMode::Rate;
        let rate = state.connections_partitions();
        let rate_ids = rate.live.iter().map(|e| e.id.as_str()).collect::<Vec<_>>();
        assert_eq!(rate_ids, vec!["b", "a", "c"]);
    }

    #[test]
    fn live_stale_partition_respects_live_window() {
        let mut state = AppState::new();
        state.connections_live_window_secs = 30;
        state.if_stats_local.tracked_connections_summary = vec![
            mk_entry("live", "10.0.0.1:1", "10.0.0.1:2", 10, 1.0),
            mk_entry("stale", "10.0.0.2:1", "10.0.0.2:2", 31, 1.0),
        ];

        let p = state.connections_partitions();
        assert_eq!(p.live.len(), 1);
        assert_eq!(p.stale.len(), 1);
        assert_eq!(p.live[0].id, "live");
        assert_eq!(p.stale[0].id, "stale");
    }

    #[test]
    fn stale_hidden_does_not_panic_or_clear_data() {
        let mut state = AppState::new();
        state.connections_live_window_secs = 30;
        state.connections_show_stale_panel = false;
        state.if_stats_local.tracked_connections_summary =
            vec![mk_entry("only-stale", "10.0.0.1:1", "10.0.0.1:2", 80, 0.2)];

        let p = state.connections_partitions();
        state.sync_connections_selection(&p.live, &p.stale);

        assert!(p.live.is_empty());
        assert_eq!(p.stale.len(), 1);
        assert_eq!(state.connections_selected_id.as_deref(), Some("only-stale"));
    }

    #[test]
    fn decoded_selection_remains_pinned_across_refresh() {
        let mut state = AppState::new();
        state.if_stats_local.events = vec![
            mk_event(100, "GET", "10.0.0.1", "10.0.0.2", "alpha"),
            mk_event(101, "PUT", "10.0.0.3", "10.0.0.4", "beta"),
        ];
        let rows = state.decoded_rows();
        let pinned = rows[1].id.clone();
        state.decoded_selected_id = Some(pinned.clone());

        state.if_stats_local.events.push(mk_event(
            102,
            "MONITOR",
            "10.0.0.9",
            "10.0.0.10",
            "gamma",
        ));
        let rows2 = state.decoded_rows();
        state.sync_decoded_selection(&rows2);
        assert_eq!(state.decoded_selected_id.as_deref(), Some(pinned.as_str()));
    }

    #[test]
    fn decoded_selection_falls_back_when_selected_row_disappears() {
        let mut state = AppState::new();
        state.if_stats_local.events = vec![
            mk_event(100, "GET", "10.0.0.1", "10.0.0.2", "alpha"),
            mk_event(101, "PUT", "10.0.0.3", "10.0.0.4", "beta"),
        ];
        let rows = state.decoded_rows();
        state.decoded_selected_id = Some(rows[1].id.clone());

        state.if_stats_local.events = vec![mk_event(102, "GET", "10.0.0.5", "10.0.0.6", "only")];
        let rows2 = state.decoded_rows();
        state.sync_decoded_selection(&rows2);
        assert_eq!(
            state.decoded_selected_id.as_deref(),
            Some(rows2[0].id.as_str())
        );
    }

    #[test]
    fn decoded_newest_first_sort_is_deterministic() {
        let mut state = AppState::new();
        state.decoded_sort_mode = DecodedSortMode::NewestFirst;
        state.if_stats_local.events = vec![
            mk_event(50, "GET", "10.0.0.1", "10.0.0.2", "a"),
            mk_event(52, "GET", "10.0.0.1", "10.0.0.2", "b"),
            mk_event(51, "GET", "10.0.0.1", "10.0.0.2", "c"),
        ];
        let rows = state.decoded_rows();
        assert!(rows[0].ts_nanos > rows[1].ts_nanos);
        assert!(rows[1].ts_nanos > rows[2].ts_nanos);
    }

    #[test]
    fn decoded_rows_respect_event_filter_selection() {
        let mut state = AppState::new();
        state.if_stats_local.events = vec![
            mk_event(100, "GET", "10.0.0.1", "10.0.0.2", "pv=A"),
            mk_event(101, "PUT", "10.0.0.1", "10.0.0.3", "pv=B"),
        ];
        state.event_filter.insert("PUT".to_string(), false);

        let rows = state.decoded_rows();

        assert_eq!(rows.len(), 1);
        assert_eq!(rows[0].request_type, "GET");
    }

    #[test]
    fn decoded_text_filter_matches_anywhere() {
        let mut state = AppState::new();
        state.if_stats_local.events = vec![
            mk_event(100, "GET", "10.0.0.1", "10.0.0.2", "pv=alpha"),
            mk_event(101, "PUT", "10.0.1.1", "10.0.1.2", "pv=beta"),
        ];
        state.decoded_filter_text = "10.0.1.1".to_string();

        let rows = state.decoded_rows();

        assert_eq!(rows.len(), 1);
        assert_eq!(rows[0].request_type, "PUT");
    }

    #[test]
    fn dashboard_top_n_counts_respects_cap_and_order() {
        let mut map = HashMap::new();
        map.insert("a".to_string(), 3);
        map.insert("b".to_string(), 10);
        map.insert("c".to_string(), 7);
        let top = AppState::top_n_counts(&map, 2);
        assert_eq!(top.len(), 2);
        assert_eq!(top[0].0, "b");
        assert_eq!(top[1].0, "c");
    }
}
