//! Presentation state for a storage-provided password-store tree.

use std::collections::BTreeSet;

use ironstorage::read::{FindResults, TreeModel, TreeNode, TreeNodeIndicators, TreeNodeKind};

#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct NodeId {
    path: String,
    directory: bool,
}

impl NodeId {
    pub fn path(&self) -> &str {
        &self.path
    }

    pub fn is_directory(&self) -> bool {
        self.directory
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct SidebarNode {
    id: NodeId,
    name: String,
    indicators: TreeNodeIndicators,
    children: Vec<SidebarNode>,
}

impl SidebarNode {
    fn from_storage(node: &TreeNode) -> Self {
        Self {
            id: NodeId {
                path: node.path().to_owned(),
                directory: node.kind() == TreeNodeKind::Directory,
            },
            name: node.name().to_owned(),
            indicators: node.indicators(),
            children: node.children().iter().map(Self::from_storage).collect(),
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VisibleRow {
    pub id: NodeId,
    pub name: String,
    pub depth: usize,
    pub indicators: TreeNodeIndicators,
    pub expanded: bool,
    pub has_children: bool,
    parent: Option<NodeId>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SidebarIntent {
    None,
    OpenEntry(String),
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Sidebar {
    full: Vec<SidebarNode>,
    filtered: Option<Vec<SidebarNode>>,
    filtered_matches: BTreeSet<NodeId>,
    expanded: BTreeSet<NodeId>,
    selected: Option<NodeId>,
    scroll: usize,
    viewport_height: usize,
    filter_query: String,
    editing_filter: bool,
}

impl Sidebar {
    pub fn replace_tree(&mut self, model: &TreeModel) {
        let previous_index = self.selected_index().unwrap_or(0);
        self.full = model
            .children()
            .iter()
            .map(SidebarNode::from_storage)
            .collect();
        self.filtered = None;
        self.filtered_matches.clear();
        self.filter_query.clear();
        self.editing_filter = false;
        self.retain_valid_state(previous_index);
    }

    pub fn apply_filter_results(&mut self, query: &str, results: &FindResults) -> bool {
        if query != self.filter_query {
            return false;
        }
        let previous_index = self.selected_index().unwrap_or(0);
        self.filtered = Some(
            results
                .tree()
                .children()
                .iter()
                .map(SidebarNode::from_storage)
                .collect(),
        );
        self.filtered_matches = results
            .matches()
            .iter()
            .map(|matched| NodeId {
                path: matched.path().to_owned(),
                directory: matched.kind() == TreeNodeKind::Directory,
            })
            .collect();
        self.retain_valid_state(previous_index);
        true
    }

    pub fn apply_find_results(&mut self, query: &str, results: &FindResults) {
        self.filter_query = query.to_owned();
        self.editing_filter = false;
        let _applied = self.apply_filter_results(query, results);
    }

    pub fn clear_filter_results(&mut self) {
        let previous_index = self.selected_index().unwrap_or(0);
        self.filter_query.clear();
        self.filtered = None;
        self.filtered_matches.clear();
        self.editing_filter = false;
        self.retain_valid_state(previous_index);
    }

    pub fn reset_filter_results(&mut self) {
        let previous_index = self.selected_index().unwrap_or(0);
        self.filtered = None;
        self.filtered_matches.clear();
        self.retain_valid_state(previous_index);
    }

    pub fn begin_filter(&mut self) {
        self.editing_filter = true;
    }

    pub fn finish_filter(&mut self) {
        self.editing_filter = false;
    }

    pub fn is_editing_filter(&self) -> bool {
        self.editing_filter
    }

    pub fn filter_query(&self) -> &str {
        &self.filter_query
    }

    pub fn push_filter_character(&mut self, character: char) -> bool {
        if character.is_control() {
            return false;
        }
        self.filter_query.push(character);
        true
    }

    pub fn pop_filter_character(&mut self) -> bool {
        self.filter_query.pop().is_some()
    }

    pub fn selected(&self) -> Option<&NodeId> {
        self.selected.as_ref()
    }

    pub fn select_path(&mut self, path: &str, directory: bool) -> bool {
        let target = collect_ids(&self.full)
            .into_iter()
            .find(|id| id.path() == path && id.is_directory() == directory);
        let Some(target) = target else {
            return false;
        };
        if !path.is_empty() {
            let mut prefix = String::new();
            for component in path
                .split('/')
                .take(path.split('/').count().saturating_sub(1))
            {
                if !prefix.is_empty() {
                    prefix.push('/');
                }
                prefix.push_str(component);
                self.expanded.insert(NodeId {
                    path: prefix.clone(),
                    directory: true,
                });
            }
        }
        self.selected = Some(target);
        self.ensure_selected_visible();
        true
    }

    pub fn completion_paths(&self) -> Vec<String> {
        fn visit(nodes: &[SidebarNode], paths: &mut Vec<String>) {
            for node in nodes {
                paths.push(node.id.path.clone());
                visit(&node.children, paths);
            }
        }

        let mut paths = Vec::new();
        visit(&self.full, &mut paths);
        paths
    }

    pub fn set_viewport_height(&mut self, height: usize) {
        self.viewport_height = height.max(1);
        self.ensure_selected_visible();
    }

    pub fn visible_rows(&self) -> Vec<VisibleRow> {
        let mut rows = Vec::new();
        let filtering = !self.filter_query.is_empty();
        flatten(
            self.active_nodes(),
            0,
            None,
            &self.expanded,
            filtering,
            &mut rows,
        );
        rows
    }

    pub fn visible_window(&self) -> Vec<VisibleRow> {
        self.visible_rows()
            .into_iter()
            .skip(self.scroll)
            .take(self.viewport_height.max(1))
            .collect()
    }

    pub fn move_next(&mut self) {
        self.move_by(1);
    }

    pub fn move_previous(&mut self) {
        self.move_by(-1);
    }

    pub fn page_down(&mut self) {
        self.move_by(self.viewport_height.max(1) as isize);
    }

    pub fn page_up(&mut self) {
        self.move_by(-(self.viewport_height.max(1) as isize));
    }

    pub fn move_first(&mut self) {
        self.select_index(0);
    }

    pub fn move_last(&mut self) {
        let rows = self.visible_rows();
        if !rows.is_empty() {
            self.select_index(rows.len() - 1);
        }
    }

    pub fn move_parent(&mut self) {
        let rows = self.visible_rows();
        let Some(index) = selected_index_in(&rows, self.selected.as_ref()) else {
            return;
        };
        if let Some(parent) = &rows[index].parent {
            self.selected = Some(parent.clone());
            self.ensure_selected_visible();
        }
    }

    pub fn move_child(&mut self) {
        let rows = self.visible_rows();
        let Some(index) = selected_index_in(&rows, self.selected.as_ref()) else {
            return;
        };
        let row = &rows[index];
        if !row.id.is_directory() || !row.has_children {
            return;
        }
        self.expanded.insert(row.id.clone());
        let rows = self.visible_rows();
        if let Some(child) = rows.get(index + 1).filter(|child| child.depth > row.depth) {
            self.selected = Some(child.id.clone());
            self.ensure_selected_visible();
        }
    }

    pub fn collapse_or_parent(&mut self) {
        let Some(selected) = self.selected.clone() else {
            return;
        };
        if selected.is_directory() && self.expanded.remove(&selected) {
            self.ensure_selected_visible();
        } else {
            self.move_parent();
        }
    }

    pub fn activate(&mut self) -> SidebarIntent {
        let Some(selected) = self.selected.clone() else {
            return SidebarIntent::None;
        };
        if selected.is_directory() {
            if !self.expanded.remove(&selected) {
                self.expanded.insert(selected);
            }
            self.ensure_selected_visible();
            SidebarIntent::None
        } else {
            SidebarIntent::OpenEntry(selected.path)
        }
    }

    pub fn next_match(&mut self) {
        if self.filter_query.is_empty() {
            return;
        }
        let matches = self.match_rows();
        if matches.is_empty() {
            return;
        }
        let next = matches
            .iter()
            .position(|row| Some(&row.id) == self.selected.as_ref())
            .map_or(0, |index| (index + 1) % matches.len());
        self.selected = Some(matches[next].id.clone());
        self.ensure_selected_visible();
    }

    pub fn previous_match(&mut self) {
        if self.filter_query.is_empty() {
            return;
        }
        let matches = self.match_rows();
        if matches.is_empty() {
            return;
        }
        let previous = matches
            .iter()
            .position(|row| Some(&row.id) == self.selected.as_ref())
            .map_or(0, |index| index.checked_sub(1).unwrap_or(matches.len() - 1));
        self.selected = Some(matches[previous].id.clone());
        self.ensure_selected_visible();
    }

    fn active_nodes(&self) -> &[SidebarNode] {
        self.filtered.as_deref().unwrap_or(&self.full)
    }

    fn selected_index(&self) -> Option<usize> {
        selected_index_in(&self.visible_rows(), self.selected.as_ref())
    }

    fn match_rows(&self) -> Vec<VisibleRow> {
        self.visible_rows()
            .into_iter()
            .filter(|row| self.filtered_matches.contains(&row.id))
            .collect()
    }

    fn move_by(&mut self, amount: isize) {
        let rows = self.visible_rows();
        if rows.is_empty() {
            self.selected = None;
            self.scroll = 0;
            return;
        }
        let current = selected_index_in(&rows, self.selected.as_ref()).unwrap_or(0);
        let destination = current.saturating_add_signed(amount).min(rows.len() - 1);
        self.select_index(destination);
    }

    fn select_index(&mut self, index: usize) {
        if let Some(row) = self.visible_rows().get(index) {
            self.selected = Some(row.id.clone());
            self.ensure_selected_visible();
        }
    }

    fn ensure_selected_visible(&mut self) {
        let rows = self.visible_rows();
        let Some(index) = selected_index_in(&rows, self.selected.as_ref()) else {
            self.scroll = self.scroll.min(rows.len().saturating_sub(1));
            return;
        };
        if index < self.scroll {
            self.scroll = index;
        } else if index >= self.scroll + self.viewport_height.max(1) {
            self.scroll = index + 1 - self.viewport_height.max(1);
        }
    }

    fn retain_valid_state(&mut self, previous_index: usize) {
        let all_ids = collect_ids(&self.full);
        self.expanded.retain(|id| all_ids.contains(id));
        let rows = self.visible_rows();
        if !rows
            .iter()
            .any(|row| Some(&row.id) == self.selected.as_ref())
        {
            self.selected = rows
                .get(previous_index.min(rows.len().saturating_sub(1)))
                .map(|row| row.id.clone());
        }
        self.ensure_selected_visible();
    }

    #[cfg(test)]
    pub(crate) fn replace_test_tree(&mut self, nodes: Vec<TestTreeNode>) {
        self.full = nodes.into_iter().map(TestTreeNode::into_sidebar).collect();
        self.filtered = None;
        self.filtered_matches.clear();
        self.retain_valid_state(0);
    }
}

#[cfg(test)]
pub(crate) struct TestTreeNode {
    pub path: String,
    pub name: String,
    pub directory: bool,
    pub indicators: TreeNodeIndicators,
    pub children: Vec<TestTreeNode>,
}

#[cfg(test)]
impl TestTreeNode {
    fn into_sidebar(self) -> SidebarNode {
        SidebarNode {
            id: NodeId {
                path: self.path,
                directory: self.directory,
            },
            name: self.name,
            indicators: self.indicators,
            children: self.children.into_iter().map(Self::into_sidebar).collect(),
        }
    }
}

fn flatten(
    nodes: &[SidebarNode],
    depth: usize,
    parent: Option<&NodeId>,
    expanded: &BTreeSet<NodeId>,
    force_expanded: bool,
    rows: &mut Vec<VisibleRow>,
) {
    for node in nodes {
        let is_expanded = force_expanded || expanded.contains(&node.id);
        rows.push(VisibleRow {
            id: node.id.clone(),
            name: node.name.clone(),
            depth,
            indicators: node.indicators,
            expanded: is_expanded,
            has_children: !node.children.is_empty(),
            parent: parent.cloned(),
        });
        if node.id.is_directory() && is_expanded {
            flatten(
                &node.children,
                depth + 1,
                Some(&node.id),
                expanded,
                force_expanded,
                rows,
            );
        }
    }
}

fn selected_index_in(rows: &[VisibleRow], selected: Option<&NodeId>) -> Option<usize> {
    let selected = selected?;
    rows.iter().position(|row| &row.id == selected)
}

fn collect_ids(nodes: &[SidebarNode]) -> BTreeSet<NodeId> {
    fn visit(nodes: &[SidebarNode], ids: &mut BTreeSet<NodeId>) {
        for node in nodes {
            ids.insert(node.id.clone());
            visit(&node.children, ids);
        }
    }
    let mut ids = BTreeSet::new();
    visit(nodes, &mut ids);
    ids
}

#[cfg(test)]
mod tests {
    use super::*;

    fn node(path: &str, directory: bool, children: Vec<SidebarNode>) -> SidebarNode {
        SidebarNode {
            id: NodeId {
                path: path.to_owned(),
                directory,
            },
            name: path.rsplit('/').next().unwrap_or(path).to_owned(),
            indicators: TreeNodeIndicators::default(),
            children,
        }
    }

    fn sidebar() -> Sidebar {
        let mut sidebar = Sidebar {
            full: vec![
                node(
                    "personal",
                    true,
                    vec![
                        node("personal/email", false, vec![]),
                        node("personal/咖啡", false, vec![]),
                    ],
                ),
                node("work", true, vec![node("work/server", false, vec![])]),
            ],
            viewport_height: 3,
            ..Sidebar::default()
        };
        sidebar.retain_valid_state(0);
        sidebar
    }

    #[test]
    fn navigation_is_deterministic_and_directories_are_collapsible() {
        let mut sidebar = sidebar();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal"));
        sidebar.move_child();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/email"));
        sidebar.move_next();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/咖啡"));
        sidebar.move_parent();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal"));
        sidebar.collapse_or_parent();
        assert_eq!(sidebar.visible_rows().len(), 2);
        sidebar.move_last();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("work"));
        sidebar.move_first();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal"));
    }

    #[test]
    fn paging_clamps_and_scrolls_large_trees() {
        let mut sidebar = Sidebar {
            full: (0..1000)
                .map(|index| node(&format!("entry-{index:04}"), false, vec![]))
                .collect(),
            viewport_height: 5,
            ..Sidebar::default()
        };
        sidebar.retain_valid_state(0);
        sidebar.page_down();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("entry-0005"));
        assert_eq!(sidebar.scroll, 1);
        sidebar.move_last();
        sidebar.page_down();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("entry-0999"));
        assert_eq!(sidebar.visible_window().len(), 5);
        sidebar.page_up();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("entry-0994"));
    }

    #[test]
    fn refresh_preserves_valid_state_and_repairs_deleted_selection() {
        let mut sidebar = sidebar();
        sidebar.move_child();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/email"));
        sidebar.full[0].children.remove(0);
        sidebar.retain_valid_state(1);
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/咖啡"));
        assert!(
            sidebar
                .visible_rows()
                .iter()
                .any(|row| Some(&row.id) == sidebar.selected())
        );
    }

    #[test]
    fn filtering_uses_supplied_results_and_cycles_matches() {
        let mut sidebar = sidebar();
        sidebar.begin_filter();
        assert!(sidebar.push_filter_character('咖'));
        sidebar.filtered = Some(vec![node(
            "personal",
            true,
            vec![node("personal/咖啡", false, vec![])],
        )]);
        sidebar.filtered_matches = [NodeId {
            path: "personal/咖啡".to_owned(),
            directory: false,
        }]
        .into_iter()
        .collect();
        sidebar.retain_valid_state(0);
        assert_eq!(sidebar.visible_rows().len(), 2);
        sidebar.next_match();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/咖啡"));
        sidebar.next_match();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/咖啡"));
        sidebar.previous_match();
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/咖啡"));
        sidebar.clear_filter_results();
        assert_eq!(sidebar.visible_rows().len(), 2);
    }

    #[test]
    fn activation_never_opens_a_directory_as_an_entry() {
        let mut sidebar = sidebar();
        assert_eq!(sidebar.activate(), SidebarIntent::None);
        sidebar.move_next();
        assert_eq!(
            sidebar.activate(),
            SidebarIntent::OpenEntry("personal/email".to_owned())
        );
    }

    #[test]
    fn command_selection_expands_ancestors_and_completion_uses_the_full_tree() {
        let mut sidebar = sidebar();
        assert!(sidebar.select_path("personal", true));
        assert!(sidebar.select_path("personal/咖啡", false));
        assert_eq!(sidebar.selected().map(NodeId::path), Some("personal/咖啡"));
        assert!(
            sidebar
                .visible_rows()
                .iter()
                .any(|row| row.id.path() == "personal/咖啡")
        );
        assert_eq!(
            sidebar.completion_paths(),
            vec![
                "personal",
                "personal/email",
                "personal/咖啡",
                "work",
                "work/server"
            ]
        );
        assert!(!sidebar.select_path("personal/咖啡", true));
    }

    #[test]
    fn empty_tree_has_no_stale_selection() {
        let mut sidebar = Sidebar::default();
        sidebar.retain_valid_state(0);
        sidebar.move_next();
        sidebar.page_down();
        assert_eq!(sidebar.selected(), None);
        assert!(sidebar.visible_window().is_empty());
    }

    #[test]
    fn deep_trees_and_entry_directory_name_collisions_keep_stable_identities() {
        let mut nested = node("depth-128/entry", false, vec![]);
        for depth in (0..128).rev() {
            nested = node(&format!("depth-{depth}"), true, vec![nested]);
        }
        let collision_entry = node("shared", false, vec![]);
        let collision_directory = node("shared", true, vec![node("shared/child", false, vec![])]);
        let mut sidebar = Sidebar {
            full: vec![nested, collision_directory, collision_entry],
            viewport_height: 10,
            filter_query: "force expansion".to_owned(),
            ..Sidebar::default()
        };
        sidebar.retain_valid_state(0);
        let rows = sidebar.visible_rows();
        assert_eq!(rows.iter().map(|row| row.depth).max(), Some(128));
        let collisions = rows
            .iter()
            .filter(|row| row.id.path() == "shared")
            .collect::<Vec<_>>();
        assert_eq!(collisions.len(), 2);
        assert_ne!(collisions[0].id, collisions[1].id);
    }
}
