//! Fuzzy search and host filtering logic

use crate::inventory::{FolderId, InventoryTreeModel, TreeFolder, build_inventory_tree};
use crate::tui::state::HostSearchEntry;
use crate::tui::{AppState, HostTreeRow, HostTreeRowKind};
use std::collections::{HashMap, HashSet};
use std::io;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct FolderRowKey {
    source_path: PathBuf,
    names: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum HostRowKey {
    Folder(FolderRowKey),
    Host(String),
}

fn empty_inventory_tree_model(inventory_path: &Path) -> InventoryTreeModel {
    let root_name = inventory_path
        .file_name()
        .and_then(|segment| segment.to_str())
        .unwrap_or("cossh-inventory.yaml")
        .to_string();

    InventoryTreeModel {
        root: TreeFolder {
            id: 0,
            name: root_name,
            path: inventory_path.to_path_buf(),
            children: Vec::new(),
            host_indices: Vec::new(),
        },
        hosts: Vec::new(),
    }
}

fn host_row_indent(depth: usize) -> String {
    "  ".repeat(depth)
}

/// Fuzzy match scoring for host search.
///
/// Returns a score if `pattern_lower` fuzzy-matches against `text_lower`, or
/// `None` if there is no match. Consecutive character matches score higher.
fn fuzzy_match(text_lower: &str, pattern_lower: &str) -> Option<i32> {
    let text = text_lower;
    let pattern = pattern_lower;

    let mut text_chars = text.chars().peekable();
    let mut pattern_chars = pattern.chars().peekable();
    let mut score = 0;
    let mut consecutive = 0;

    while let Some(&pattern_char) = pattern_chars.peek() {
        let mut found = false;

        while let Some(&text_char) = text_chars.peek() {
            text_chars.next();
            if text_char == pattern_char {
                score += 1 + consecutive;
                consecutive += 1;
                pattern_chars.next();
                found = true;
                break;
            }
            consecutive = 0;
        }

        if !found {
            return None; // Pattern not found
        }
    }

    Some(score)
}

/// Strict contiguous match scoring.
///
/// Higher score for prefix matches, then earlier substring positions.
fn strict_match_score(text_lower: &str, pattern_lower: &str) -> Option<i32> {
    let pos = text_lower.find(pattern_lower)?;

    if pos == 0 {
        Some(300 + pattern_lower.len() as i32)
    } else {
        Some((200 - pos as i32).max(1))
    }
}

fn compute_match_scores(search_entries: &[HostSearchEntry], query_lower: &str) -> HashMap<usize, i32> {
    let mut match_scores = HashMap::new();

    // Pass 1: strict contiguous matching.
    for (idx, search_entry) in search_entries.iter().enumerate() {
        if search_entry.hidden {
            continue;
        }

        let mut best_score = None;

        if let Some(score) = strict_match_score(&search_entry.name_lower, query_lower) {
            best_score = Some(score + 1000);
        }

        if let Some(hostname) = &search_entry.host_lower
            && let Some(score) = strict_match_score(hostname, query_lower)
        {
            best_score = Some(best_score.unwrap_or(0).max(score + 500));
        }

        if let Some(user) = &search_entry.user_lower
            && let Some(score) = strict_match_score(user, query_lower)
        {
            best_score = Some(best_score.unwrap_or(0).max(score + 300));
        }

        if let Some(score) = best_score {
            match_scores.insert(idx, score);
        }
    }

    if !match_scores.is_empty() {
        return match_scores;
    }

    // Pass 2: fuzzy fallback when strict matching found nothing.
    for (idx, search_entry) in search_entries.iter().enumerate() {
        if search_entry.hidden {
            continue;
        }

        let mut best_score = None;

        if let Some(score) = fuzzy_match(&search_entry.name_lower, query_lower) {
            best_score = Some(score + 100);
        }

        if let Some(hostname) = &search_entry.host_lower
            && let Some(score) = fuzzy_match(hostname, query_lower)
        {
            best_score = Some(best_score.unwrap_or(0).max(score + 50));
        }

        if let Some(user) = &search_entry.user_lower
            && let Some(score) = fuzzy_match(user, query_lower)
        {
            best_score = Some(best_score.unwrap_or(0).max(score + 30));
        }

        if let Some(score) = best_score {
            match_scores.insert(idx, score);
        }
    }

    match_scores
}

impl AppState {
    // Row identity helpers.
    fn folder_key_by_id_recursive(folder: &TreeFolder, folder_id: FolderId, names: &mut Vec<String>) -> Option<FolderRowKey> {
        if folder.id == folder_id {
            return Some(FolderRowKey {
                source_path: folder.path.clone(),
                names: names.clone(),
            });
        }

        for child in &folder.children {
            names.push(child.name.clone());
            if let Some(key) = Self::folder_key_by_id_recursive(child, folder_id, names) {
                return Some(key);
            }
            names.pop();
        }

        None
    }

    fn folder_key_by_id(&self, folder_id: FolderId) -> Option<FolderRowKey> {
        let mut names = Vec::new();
        Self::folder_key_by_id_recursive(&self.host_tree_root, folder_id, &mut names)
    }

    fn folder_id_from_key_recursive(folder: &TreeFolder, key: &FolderRowKey, names: &mut Vec<String>) -> Option<FolderId> {
        if folder.path == key.source_path && names.as_slice() == key.names.as_slice() {
            return Some(folder.id);
        }

        for child in &folder.children {
            names.push(child.name.clone());
            if let Some(folder_id) = Self::folder_id_from_key_recursive(child, key, names) {
                return Some(folder_id);
            }
            names.pop();
        }

        None
    }

    fn folder_id_from_key(&self, key: &FolderRowKey) -> Option<FolderId> {
        let mut names = Vec::new();
        Self::folder_id_from_key_recursive(&self.host_tree_root, key, &mut names)
    }

    fn row_key_from_kind(&self, kind: HostTreeRowKind) -> Option<HostRowKey> {
        match kind {
            HostTreeRowKind::Folder(id) => self.folder_key_by_id(id).map(HostRowKey::Folder),
            HostTreeRowKind::Host(idx) => self.hosts.get(idx).map(|host| HostRowKey::Host(host.name.clone())),
        }
    }

    fn selected_row_key(&self) -> Option<HostRowKey> {
        self.visible_host_rows
            .get(self.selected_host_row)
            .and_then(|row| self.row_key_from_kind(row.kind))
    }

    fn collapsed_folder_keys(&self) -> Vec<FolderRowKey> {
        self.collapsed_folders
            .iter()
            .filter_map(|folder_id| self.folder_key_by_id(*folder_id))
            .collect()
    }

    fn restore_collapsed_folders(&mut self, folder_keys: &[FolderRowKey]) {
        self.collapsed_folders = folder_keys.iter().filter_map(|key| self.folder_id_from_key(key)).collect::<HashSet<_>>();
    }

    fn row_matches_key(&self, row: &HostTreeRow, key: &HostRowKey) -> bool {
        self.row_key_from_kind(row.kind).as_ref() == Some(key)
    }

    // Visible row rebuild pipeline.
    fn rebuild_visible_host_rows(&mut self) {
        let mut rows = Vec::new();
        if self.search_query.is_empty() {
            self.collect_root_visible_rows_normal(&mut rows);
        } else {
            self.collect_root_visible_rows_search(&mut rows);
        }
        self.visible_host_rows = rows;
    }

    // Tree walk in normal mode.
    /// Build visible rows in normal mode while keeping the synthetic root hidden.
    fn collect_root_visible_rows_normal(&self, rows: &mut Vec<HostTreeRow>) {
        for &host_idx in &self.host_tree_root.host_indices {
            if let Some(host) = self.hosts.get(host_idx) {
                if host.hidden {
                    continue;
                }
                rows.push(HostTreeRow {
                    kind: HostTreeRowKind::Host(host_idx),
                    indent: host_row_indent(0),
                    display_name: host.name.clone(),
                    expanded: false,
                });
            }
        }

        for child in &self.host_tree_root.children {
            self.collect_visible_rows_normal(child, 0, rows);
        }
    }

    // Tree walk in search mode.
    /// Build visible rows in search mode while keeping the synthetic root hidden.
    fn collect_root_visible_rows_search(&self, rows: &mut Vec<HostTreeRow>) {
        for &host_idx in &self.host_tree_root.host_indices {
            if self.host_match_scores.contains_key(&host_idx)
                && let Some(host) = self.hosts.get(host_idx)
            {
                if host.hidden {
                    continue;
                }
                rows.push(HostTreeRow {
                    kind: HostTreeRowKind::Host(host_idx),
                    indent: host_row_indent(0),
                    display_name: host.name.clone(),
                    expanded: false,
                });
            }
        }

        for child in &self.host_tree_root.children {
            let mut sub_rows = Vec::new();
            if self.collect_visible_rows_search(child, 0, &mut sub_rows) {
                rows.extend(sub_rows);
            }
        }
    }

    fn collect_visible_rows_normal(&self, folder: &TreeFolder, depth: usize, rows: &mut Vec<HostTreeRow>) {
        let expanded = !self.collapsed_folders.contains(&folder.id);
        rows.push(HostTreeRow {
            kind: HostTreeRowKind::Folder(folder.id),
            indent: host_row_indent(depth),
            display_name: folder.name.clone(),
            expanded,
        });

        if !expanded {
            return;
        }

        for &host_idx in &folder.host_indices {
            if let Some(host) = self.hosts.get(host_idx) {
                if host.hidden {
                    continue;
                }
                rows.push(HostTreeRow {
                    kind: HostTreeRowKind::Host(host_idx),
                    indent: host_row_indent(depth + 1),
                    display_name: host.name.clone(),
                    expanded: false,
                });
            }
        }

        for child in &folder.children {
            self.collect_visible_rows_normal(child, depth + 1, rows);
        }
    }

    /// Collect search-filtered rows and return whether this folder has a matching descendant.
    fn collect_visible_rows_search(&self, folder: &TreeFolder, depth: usize, rows: &mut Vec<HostTreeRow>) -> bool {
        let mut child_rows = Vec::new();
        let mut has_child_match = false;
        for child in &folder.children {
            let mut sub_rows = Vec::new();
            let child_has = self.collect_visible_rows_search(child, depth + 1, &mut sub_rows);
            if child_has {
                has_child_match = true;
                child_rows.extend(sub_rows);
            }
        }

        let mut host_rows = Vec::new();
        for &host_idx in &folder.host_indices {
            if self.host_match_scores.contains_key(&host_idx)
                && let Some(host) = self.hosts.get(host_idx)
            {
                if host.hidden {
                    continue;
                }
                host_rows.push(HostTreeRow {
                    kind: HostTreeRowKind::Host(host_idx),
                    indent: host_row_indent(depth + 1),
                    display_name: host.name.clone(),
                    expanded: false,
                });
            }
        }

        let has_match = !host_rows.is_empty() || has_child_match;
        if has_match {
            rows.push(HostTreeRow {
                kind: HostTreeRowKind::Folder(folder.id),
                indent: host_row_indent(depth),
                display_name: folder.name.clone(),
                expanded: true,
            });
            rows.extend(host_rows);
            rows.extend(child_rows);
        }

        has_match
    }

    // Selection stabilization after list rebuilds.
    fn sync_host_row_selection_state(&mut self) {
        if self.visible_host_rows.is_empty() {
            self.selected_host_row = 0;
            return;
        }

        if self.selected_host_row >= self.visible_host_rows.len() {
            self.selected_host_row = self.visible_host_rows.len().saturating_sub(1);
        }
    }

    fn repair_selection_after_rebuild(&mut self, preferred: Option<HostRowKey>) {
        if self.visible_host_rows.is_empty() {
            self.selected_host_row = 0;
            self.host_scroll_offset = 0;
            return;
        }

        if let Some(key) = preferred
            && let Some(idx) = self.visible_host_rows.iter().position(|row| self.row_matches_key(row, &key))
        {
            self.selected_host_row = idx;
            self.sync_host_row_selection_state();
            return;
        }

        self.selected_host_row = 0;
        self.sync_host_row_selection_state();
    }

    // Public filter entry point.
    /// Update the filtered hosts based on search query with fuzzy matching.
    fn update_filtered_hosts_with_preferred(&mut self, preferred: Option<HostRowKey>) {
        self.host_match_scores.clear();
        if !self.search_query.is_empty() {
            let query_lower = self.search_query.to_lowercase();
            self.host_match_scores = compute_match_scores(&self.host_search_index, &query_lower);
        }

        self.rebuild_visible_host_rows();
        self.host_scroll_offset = 0;
        self.repair_selection_after_rebuild(preferred);
    }

    pub(crate) fn update_filtered_hosts(&mut self) {
        let previous = self.selected_row_key();
        self.update_filtered_hosts_with_preferred(previous);
    }

    pub(crate) fn reload_inventory_tree_from_path(&mut self, inventory_path: &Path) -> io::Result<()> {
        let previous_selection = self.selected_row_key();
        let collapsed_folder_keys = self.collapsed_folder_keys();
        let previous_scroll_offset = self.host_scroll_offset;
        let tree_model = if inventory_path.exists() {
            match build_inventory_tree(inventory_path) {
                Ok(tree_model) => tree_model,
                Err(err) => {
                    self.inventory_load_error = Some(err.to_string());
                    self.mark_ui_dirty();
                    return Err(err);
                }
            }
        } else {
            empty_inventory_tree_model(inventory_path)
        };

        self.hosts = tree_model.hosts;
        self.host_search_index = Self::build_host_search_index(&self.hosts);
        self.host_tree_root = tree_model.root;
        self.inventory_load_error = None;
        self.restore_collapsed_folders(&collapsed_folder_keys);
        self.update_filtered_hosts_with_preferred(previous_selection);
        self.host_scroll_offset = previous_scroll_offset.min(self.visible_host_rows.len().saturating_sub(1));
        self.mark_ui_dirty();
        Ok(())
    }

    // Selection accessors.
    pub(crate) fn selected_host_idx(&self) -> Option<usize> {
        match self.visible_host_rows.get(self.selected_host_row) {
            Some(HostTreeRow {
                kind: HostTreeRowKind::Host(host_idx),
                ..
            }) => Some(*host_idx),
            _ => None,
        }
    }

    pub(crate) fn selected_folder_id(&self) -> Option<FolderId> {
        match self.visible_host_rows.get(self.selected_host_row) {
            Some(HostTreeRow {
                kind: HostTreeRowKind::Folder(folder_id),
                ..
            }) => Some(*folder_id),
            _ => None,
        }
    }

    pub(crate) fn set_selected_row(&mut self, row: usize) {
        self.selected_host_row = row;
        self.sync_host_row_selection_state();
    }

    pub(crate) fn visible_host_row_count(&self) -> usize {
        self.visible_host_rows.len()
    }

    // Match summary.
    pub(crate) fn matched_host_count(&self) -> usize {
        self.host_match_scores.len()
    }

    // Folder expansion controls.
    pub(crate) fn is_folder_expanded(&self, folder_id: FolderId) -> bool {
        if !self.search_query.is_empty() {
            return true;
        }
        !self.collapsed_folders.contains(&folder_id)
    }

    pub(crate) fn set_folder_expanded(&mut self, folder_id: FolderId, expanded: bool) {
        if !self.search_query.is_empty() {
            return;
        }

        if expanded {
            self.collapsed_folders.remove(&folder_id);
        } else {
            self.collapsed_folders.insert(folder_id);
        }

        self.rebuild_visible_host_rows();
        let preferred = self.folder_key_by_id(folder_id).map(HostRowKey::Folder);
        self.repair_selection_after_rebuild(preferred);
    }

    pub(crate) fn toggle_folder(&mut self, folder_id: FolderId) {
        let expanded = self.is_folder_expanded(folder_id);
        self.set_folder_expanded(folder_id, !expanded);
    }

    // Folder lookup/count helpers.
    fn folder_by_id_recursive(folder: &TreeFolder, folder_id: FolderId) -> Option<&TreeFolder> {
        if folder.id == folder_id {
            return Some(folder);
        }
        for child in &folder.children {
            if let Some(found) = Self::folder_by_id_recursive(child, folder_id) {
                return Some(found);
            }
        }
        None
    }

    pub(crate) fn folder_by_id(&self, folder_id: FolderId) -> Option<&TreeFolder> {
        Self::folder_by_id_recursive(&self.host_tree_root, folder_id)
    }

    fn count_hosts_recursive(folder: &TreeFolder) -> usize {
        let mut count = folder.host_indices.len();
        for child in &folder.children {
            count += Self::count_hosts_recursive(child);
        }
        count
    }

    pub(crate) fn folder_descendant_host_count(&self, folder_id: FolderId) -> usize {
        self.folder_by_id(folder_id).map(Self::count_hosts_recursive).unwrap_or(0)
    }

    // Viewport scroll alignment.
    /// Update host list scroll to keep selection visible.
    pub(crate) fn update_host_scroll(&mut self, viewport_height: usize) {
        if self.visible_host_rows.is_empty() || viewport_height == 0 {
            return;
        }

        // Symmetric scrolling: keep cursor in viewport before scrolling.
        if self.selected_host_row < self.host_scroll_offset {
            self.host_scroll_offset = self.selected_host_row;
        } else if self.selected_host_row >= self.host_scroll_offset + viewport_height {
            self.host_scroll_offset = self.selected_host_row.saturating_sub(viewport_height - 1);
        }
    }
}
