//! Desktop navigation over storage-owned tree models and identities.

use std::{collections::BTreeSet, path::Path};

use ironstorage::{
    read::{TreeModel, TreeNode, TreeNodeId, TreeNodeIndicators, TreeNodeKind},
    repository::DirectoryPath,
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NavigationKey {
    Previous,
    Next,
    Collapse,
    Expand,
    Activate,
    First,
    Last,
}

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

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

impl NavigationNode {
    fn from_storage(node: &TreeNode) -> Self {
        Self {
            id: node.id().clone(),
            name: node.name().to_owned(),
            indicators: node.indicators(),
            children: node.children().iter().map(Self::from_storage).collect(),
        }
    }

    fn entry_count(&self) -> usize {
        if self.id.is_directory() {
            self.children.iter().map(Self::entry_count).sum()
        } else {
            1
        }
    }
}

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

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct NavigationTree {
    nodes: Vec<NavigationNode>,
    expanded: BTreeSet<TreeNodeId>,
    selected: Option<TreeNodeId>,
}

impl NavigationTree {
    pub fn replace(&mut self, model: &TreeModel) {
        self.nodes = model
            .children()
            .iter()
            .map(NavigationNode::from_storage)
            .collect();
        let valid = collect_ids(&self.nodes);
        self.expanded.retain(|id| valid.contains(id));
        if !self.selected.as_ref().is_some_and(|id| valid.contains(id)) {
            self.selected = None;
        }
    }

    pub fn is_empty(&self) -> bool {
        self.nodes.is_empty()
    }

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

    pub fn rows(&self) -> Vec<NavigationRow> {
        let mut rows = Vec::new();
        flatten(&self.nodes, 0, None, &self.expanded, &mut rows);
        rows
    }

    pub fn directories(&self) -> Vec<TreeNodeId> {
        fn visit(nodes: &[NavigationNode], directories: &mut Vec<TreeNodeId>) {
            for node in nodes {
                if node.id.is_directory() {
                    directories.push(node.id.clone());
                }
                visit(&node.children, directories);
            }
        }
        let mut directories = vec![TreeNodeId::Directory(DirectoryPath::root())];
        visit(&self.nodes, &mut directories);
        directories
    }

    pub fn selected_ratio(&self) -> f32 {
        let rows = self.rows();
        let Some(index) = selected_index(&rows, self.selected.as_ref()) else {
            return 0.0;
        };
        index as f32 / rows.len().saturating_sub(1).max(1) as f32
    }

    pub fn activate(&mut self, id: TreeNodeId) -> NavigationIntent {
        if !self.rows().iter().any(|row| row.id == id) {
            return NavigationIntent::None;
        }
        self.selected = Some(id);
        self.activate_selected()
    }

    pub fn navigate(&mut self, key: NavigationKey) -> NavigationIntent {
        match key {
            NavigationKey::Previous => self.move_by(-1),
            NavigationKey::Next => self.move_by(1),
            NavigationKey::Collapse => self.collapse_or_parent(),
            NavigationKey::Expand => self.expand_or_child(),
            NavigationKey::Activate => return self.activate_selected(),
            NavigationKey::First => self.select_edge(false),
            NavigationKey::Last => self.select_edge(true),
        }
        NavigationIntent::None
    }

    pub fn select_entry_path(&mut self, path: &str) -> bool {
        let mut lineage = Vec::new();
        if !find_lineage(&self.nodes, path, &mut lineage) {
            return false;
        }
        let Some(selected) = lineage.pop() else {
            return false;
        };
        if selected.kind() != TreeNodeKind::Entry {
            return false;
        }
        self.expanded.extend(lineage);
        self.selected = Some(selected);
        true
    }

    pub fn select_id(&mut self, id: &TreeNodeId) -> bool {
        let mut lineage = Vec::new();
        if !find_id_lineage(&self.nodes, id, &mut lineage) {
            return false;
        }
        let Some(selected) = lineage.pop() else {
            return false;
        };
        self.expanded.extend(lineage);
        self.selected = Some(selected);
        true
    }

    fn activate_selected(&mut self) -> NavigationIntent {
        let Some(selected) = self.selected.clone() else {
            return NavigationIntent::None;
        };
        if selected.is_directory() {
            if !self.expanded.remove(&selected) {
                self.expanded.insert(selected);
            }
            NavigationIntent::None
        } else {
            NavigationIntent::OpenEntry(
                selected
                    .entry()
                    .expect("non-directory tree identity is an entry")
                    .to_string(),
            )
        }
    }

    fn move_by(&mut self, amount: isize) {
        let rows = self.rows();
        if rows.is_empty() {
            self.selected = None;
            return;
        }
        let destination = selected_index(&rows, self.selected.as_ref()).map_or_else(
            || usize::from(amount < 0) * (rows.len() - 1),
            |index| index.saturating_add_signed(amount).min(rows.len() - 1),
        );
        self.selected = Some(rows[destination].id.clone());
    }

    fn select_edge(&mut self, last: bool) {
        let rows = self.rows();
        self.selected = rows
            .get(if last {
                rows.len().saturating_sub(1)
            } else {
                0
            })
            .map(|row| row.id.clone());
    }

    fn collapse_or_parent(&mut self) {
        let Some(selected) = self.selected.clone() else {
            return;
        };
        if selected.is_directory() && self.expanded.remove(&selected) {
            return;
        }
        let rows = self.rows();
        self.selected = selected_index(&rows, Some(&selected))
            .and_then(|index| rows[index].parent.clone())
            .or(Some(selected));
    }

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

    #[cfg(test)]
    pub(crate) fn replace_test_nodes(&mut self, nodes: Vec<TestNode>) {
        self.nodes = nodes.into_iter().map(TestNode::into_navigation).collect();
        let valid = collect_ids(&self.nodes);
        self.expanded.retain(|id| valid.contains(id));
        if !self.selected.as_ref().is_some_and(|id| valid.contains(id)) {
            self.selected = None;
        }
    }
}

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

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

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

fn find_lineage(nodes: &[NavigationNode], path: &str, lineage: &mut Vec<TreeNodeId>) -> bool {
    for node in nodes {
        lineage.push(node.id.clone());
        if node
            .id
            .entry()
            .is_some_and(|entry| entry.as_path() == Path::new(path))
            || find_lineage(&node.children, path, lineage)
        {
            return true;
        }
        let _ = lineage.pop();
    }
    false
}

fn find_id_lineage(
    nodes: &[NavigationNode],
    target: &TreeNodeId,
    lineage: &mut Vec<TreeNodeId>,
) -> bool {
    for node in nodes {
        lineage.push(node.id.clone());
        if &node.id == target || find_id_lineage(&node.children, target, lineage) {
            return true;
        }
        lineage.pop();
    }
    false
}

#[cfg(test)]
pub(crate) struct TestNode {
    pub id: TreeNodeId,
    pub name: String,
    pub children: Vec<TestNode>,
}

#[cfg(test)]
impl TestNode {
    fn into_navigation(self) -> NavigationNode {
        NavigationNode {
            id: self.id,
            name: self.name,
            indicators: TreeNodeIndicators::default(),
            children: self
                .children
                .into_iter()
                .map(Self::into_navigation)
                .collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use ironstorage::repository::{DirectoryPath, EntryPath};

    use super::*;

    fn directory(path: &str, children: Vec<TestNode>) -> TestNode {
        TestNode {
            id: TreeNodeId::Directory(DirectoryPath::parse(path).expect("directory path")),
            name: path.rsplit('/').next().unwrap_or(path).to_owned(),
            children,
        }
    }

    fn entry(path: &str) -> TestNode {
        TestNode {
            id: TreeNodeId::Entry(EntryPath::parse(path).expect("entry path")),
            name: path.rsplit('/').next().unwrap_or(path).to_owned(),
            children: Vec::new(),
        }
    }

    fn populated() -> Vec<TestNode> {
        vec![
            directory(
                "personal",
                vec![
                    entry("personal/email"),
                    entry("personal/a very long entry name that remains complete"),
                ],
            ),
            directory("work", vec![entry("work/server")]),
        ]
    }

    #[test]
    fn keyboard_and_mouse_navigation_expand_folders_and_open_only_entries() {
        let mut tree = NavigationTree::default();
        tree.replace_test_nodes(populated());
        assert_eq!(tree.navigate(NavigationKey::Next), NavigationIntent::None);
        assert_eq!(
            tree.selected().map(TreeNodeId::path),
            Some(Path::new("personal"))
        );
        assert_eq!(tree.navigate(NavigationKey::Expand), NavigationIntent::None);
        let rows = tree.rows();
        assert_eq!(rows.len(), 4);
        assert_eq!(rows[0].entry_count, 2);
        assert_eq!(rows[1].entry_count, 1);
        assert_eq!(tree.navigate(NavigationKey::Expand), NavigationIntent::None);
        assert_eq!(
            tree.navigate(NavigationKey::Activate),
            NavigationIntent::OpenEntry("personal/email".to_owned())
        );
        assert_eq!(
            tree.navigate(NavigationKey::Collapse),
            NavigationIntent::None
        );
        assert_eq!(
            tree.selected().map(TreeNodeId::path),
            Some(Path::new("personal"))
        );
        assert_eq!(
            tree.activate(TreeNodeId::Directory(
                DirectoryPath::parse("personal").expect("path")
            )),
            NavigationIntent::None
        );
        assert_eq!(tree.rows().len(), 2);
    }

    #[test]
    fn typed_search_and_mutation_identities_expand_and_select_hidden_nodes() {
        let mut tree = NavigationTree::default();
        tree.replace_test_nodes(populated());
        let hidden = TreeNodeId::Entry(EntryPath::parse("personal/email").expect("entry"));
        assert!(tree.select_id(&hidden));
        assert_eq!(tree.selected(), Some(&hidden));
        assert!(tree.rows().iter().any(|row| row.id == hidden));
        assert_eq!(
            tree.directories(),
            vec![
                TreeNodeId::Directory(DirectoryPath::root()),
                TreeNodeId::Directory(DirectoryPath::parse("personal").expect("personal")),
                TreeNodeId::Directory(DirectoryPath::parse("work").expect("work")),
            ]
        );
    }

    #[test]
    fn refresh_preserves_valid_typed_identity_and_clears_removed_state() {
        let mut tree = NavigationTree::default();
        tree.replace_test_nodes(populated());
        assert!(tree.select_entry_path("personal/email"));
        let selected = tree.selected().cloned();
        tree.replace_test_nodes(populated());
        assert_eq!(tree.selected(), selected.as_ref());
        assert!(tree.rows().iter().any(|row| row.name.contains("very long")));

        tree.replace_test_nodes(vec![directory("personal", Vec::new())]);
        assert!(tree.selected().is_none());
        assert_eq!(tree.expanded.len(), 1);
        tree.replace_test_nodes(Vec::new());
        assert!(tree.selected().is_none());
        assert!(tree.expanded.is_empty());
        assert!(tree.rows().is_empty());
        assert_eq!(
            tree.navigate(NavigationKey::Activate),
            NavigationIntent::None
        );
    }
}
