//! Library tree: flattening logic and the left-pane component.

use std::collections::{HashMap, HashSet};

use leptos::prelude::*;
use leptos::task::spawn_local;
use web_sys::DragEvent;

use brittle_model::{Library, LibraryId, ReferenceId};

// ── Helpers ───────────────────────────────────────────────────────────────────

/// Load `id`'s children from the backend and cache them, unless already cached.
pub fn load_children_if_needed(id: String, children_cache: RwSignal<HashMap<String, Vec<Library>>>) {
    let already = children_cache.with_untracked(|c| c.contains_key(&id));
    if !already {
        spawn_local(async move {
            let lib_id = match uuid::Uuid::parse_str(&id) {
            Ok(u) => LibraryId(u),
            Err(_) => return,
        };
            match crate::tauri::list_child_libraries(&lib_id).await {
                Ok(children) => children_cache.update(|c| { c.insert(id, children); }),
                Err(e) => leptos::logging::error!("load children ({id}): {e}"),
            }
        });
    }
}

// ── Tree logic ────────────────────────────────────────────────────────────────

/// A single visible row in the rendered library tree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeRow {
    /// Library ID string (UUID).
    pub id: String,
    /// Display name.
    pub name: String,
    /// Nesting depth (0 = root).
    pub depth: usize,
    /// Whether this node is currently expanded.
    pub expanded: bool,
    /// `true` if children have not been loaded yet, or the node has children.
    /// `false` only when children were loaded and the result was empty.
    pub may_have_children: bool,
}

/// Flatten the visible portion of the library hierarchy into an ordered list.
///
/// Only nodes whose ancestors are all expanded appear in the output.
/// Children are ordered as returned by `children_cache`.
pub fn flatten_tree(
    libs: &[Library],
    children_cache: &HashMap<String, Vec<Library>>,
    expanded: &HashSet<String>,
    depth: usize,
) -> Vec<TreeRow> {
    let mut rows = Vec::new();
    for lib in libs {
        let id = lib.id.to_string();
        let is_expanded = expanded.contains(&id);
        let children = children_cache.get(&id);
        let may_have_children = children.is_none_or(|c| !c.is_empty());

        rows.push(TreeRow {
            id: id.clone(),
            name: lib.name.clone(),
            depth,
            expanded: is_expanded,
            may_have_children,
        });

        if is_expanded {
            if let Some(child_libs) = children {
                rows.extend(flatten_tree(child_libs, children_cache, expanded, depth + 1));
            }
        }
    }
    rows
}

// ── Component ─────────────────────────────────────────────────────────────────

/// Left pane: renders the library tree.
///
/// Navigation (j/k, expand/collapse) is driven entirely from the parent via
/// `cursor` and `expanded`.  Click on a row updates `cursor`.
///
/// Each tree row is also a drag-drop target: dropping a publication item onto a
/// row calls `add_to_library`, adding the reference to that library.
#[component]
pub fn LibraryTree(
    root_libs: RwSignal<Vec<Library>>,
    children_cache: RwSignal<HashMap<String, Vec<Library>>>,
    expanded: RwSignal<HashSet<String>>,
    cursor: RwSignal<usize>,
    focused: RwSignal<crate::lib_tab::Pane>,
) -> impl IntoView {
    use crate::lib_tab::Pane;
    use leptos::either::Either;

    let rows = Memo::new(move |_| {
        flatten_tree(&root_libs.get(), &children_cache.get(), &expanded.get(), 0)
    });

    // Reactively load children for any newly-expanded node.
    // Using an Effect guarantees this runs in a proper reactive owner context,
    // which makes `spawn_local` and signal updates flush correctly.
    Effect::new(move |_| {
        let exp = expanded.get(); // subscribe to expansion changes
        for id in exp {
            load_children_if_needed(id, children_cache);
        }
    });

    // Which tree-row ID (if any) is the current drag-over target.
    let drag_over_id: RwSignal<Option<String>> = RwSignal::new(None);

    view! {
        <div class="tree-pane" class:pane-focused=move || focused.get() == Pane::Tree>
            {move || {
                let row_list = rows.get();
                if row_list.is_empty() {
                    Either::Left(view! {
                        <div class="empty-state">"Open a repository to see libraries"</div>
                    })
                } else {
                    let cursor_pos = cursor.get();
                    Either::Right(view! {
                        <ul class="tree-list">
                            {row_list.into_iter().enumerate().map(|(i, row)| {
                                let indent_px = row.depth * 16;
                                let icon = if row.may_have_children {
                                    if row.expanded { "▾ " } else { "▸ " }
                                } else {
                                    "  "
                                };
                                let is_cursor = i == cursor_pos;
                                let row_may_have_children = row.may_have_children;

                                // Clone row.id for each closure that captures it.
                                let row_id_click = row.id.clone();
                                let row_id_class = row.id.clone();
                                let row_id_over  = row.id.clone();
                                let row_id_drop  = row.id.clone();

                                view! {
                                    <li
                                        class="tree-item"
                                        class:tree-cursor=is_cursor
                                        class:tree-drop-target=move || {
                                            drag_over_id.get().as_deref() == Some(row_id_class.as_str())
                                        }
                                        style=format!("padding-left: {indent_px}px")
                                        on:click=move |_| {
                                            cursor.set(i);
                                            focused.set(Pane::Tree);
                                        }
                                        on:dblclick=move |_| {
                                            if row_may_have_children {
                                                let is_open = expanded.with_untracked(|e| e.contains(&row_id_click));
                                                if is_open {
                                                    expanded.update(|e| { e.remove(&row_id_click); });
                                                } else {
                                                    expanded.update(|e| { e.insert(row_id_click.clone()); });
                                                }
                                            }
                                        }
                                        on:dragover=move |ev: DragEvent| {
                                            ev.prevent_default();
                                            drag_over_id.set(Some(row_id_over.clone()));
                                        }
                                        on:dragleave=move |_: DragEvent| {
                                            drag_over_id.set(None);
                                        }
                                        on:drop=move |ev: DragEvent| {
                                            ev.prevent_default();
                                            drag_over_id.set(None);
                                            let Some(dt) = ev.data_transfer() else { return };
                                            let Ok(ref_id_str) = dt.get_data("application/brittle-ref-id") else { return };
                                            if ref_id_str.is_empty() { return }
                                            let Ok(lib_uuid) = uuid::Uuid::parse_str(&row_id_drop) else { return };
                                            let Ok(ref_uuid) = uuid::Uuid::parse_str(&ref_id_str) else { return };
                                            let lib_id = LibraryId(lib_uuid);
                                            let ref_id = ReferenceId(ref_uuid);
                                            spawn_local(async move {
                                                if let Err(e) = crate::tauri::add_to_library(&lib_id, &ref_id).await {
                                                    leptos::logging::warn!("add_to_library: {e}");
                                                }
                                            });
                                        }
                                    >
                                        <span class="tree-icon">{icon}</span>
                                        <span class="tree-name">{row.name.clone()}</span>
                                    </li>
                                }
                            }).collect::<Vec<_>>()}
                        </ul>
                    })
                }
            }}
        </div>
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    /// Create a Library with a deterministic UUID from a small integer.
    /// `n = 1` → `00000000-0000-0000-0000-000000000001`, etc.
    fn test_lib(n: u128, name: &str, parent: Option<u128>) -> Library {
        let mut lib = Library::new(name, parent.map(|p| LibraryId(uuid::Uuid::from_u128(p))));
        lib.id = LibraryId(uuid::Uuid::from_u128(n));
        lib
    }

    fn id_str(n: u128) -> String {
        uuid::Uuid::from_u128(n).to_string()
    }

    #[test]
    fn empty_root_produces_empty_rows() {
        let rows = flatten_tree(&[], &Default::default(), &Default::default(), 0);
        assert!(rows.is_empty());
    }

    #[test]
    fn single_root_unloaded_children() {
        let root = test_lib(1, "Root", None);
        let rows = flatten_tree(&[root], &Default::default(), &Default::default(), 0);
        assert_eq!(rows.len(), 1);
        assert_eq!(rows[0].name, "Root");
        assert_eq!(rows[0].depth, 0);
        assert!(!rows[0].expanded);
        // Unknown children → may_have_children = true
        assert!(rows[0].may_have_children);
    }

    #[test]
    fn collapsed_node_hides_children() {
        let parent = test_lib(1, "Parent", None);
        let child = test_lib(2, "Child", Some(1));
        let mut cache = HashMap::new();
        cache.insert(id_str(1), vec![child]);
        // Not in expanded set → children hidden
        let rows = flatten_tree(&[parent], &cache, &Default::default(), 0);
        assert_eq!(rows.len(), 1);
        assert!(rows[0].may_have_children);
    }

    #[test]
    fn expanded_node_shows_children() {
        let parent = test_lib(1, "Parent", None);
        let child = test_lib(2, "Child", Some(1));
        let mut cache = HashMap::new();
        cache.insert(id_str(1), vec![child]);
        let mut expanded = HashSet::new();
        expanded.insert(id_str(1));

        let rows = flatten_tree(&[parent], &cache, &expanded, 0);
        assert_eq!(rows.len(), 2);
        assert_eq!(rows[0].name, "Parent");
        assert!(rows[0].expanded);
        assert_eq!(rows[1].name, "Child");
        assert_eq!(rows[1].depth, 1);
    }

    #[test]
    fn loaded_empty_children_marks_leaf() {
        let node = test_lib(1, "Node", None);
        let mut cache = HashMap::new();
        cache.insert(id_str(1), vec![]); // explicitly empty
        let rows = flatten_tree(&[node], &cache, &Default::default(), 0);
        assert_eq!(rows.len(), 1);
        assert!(!rows[0].may_have_children);
    }

    #[test]
    fn multi_level_nesting() {
        let root = test_lib(1, "Root", None);
        let mid = test_lib(2, "Mid", Some(1));
        let leaf = test_lib(3, "Leaf", Some(2));

        let mut cache = HashMap::new();
        cache.insert(id_str(1), vec![mid]);
        cache.insert(id_str(2), vec![leaf]);

        let mut expanded = HashSet::new();
        expanded.insert(id_str(1));
        expanded.insert(id_str(2));

        let rows = flatten_tree(&[root], &cache, &expanded, 0);
        assert_eq!(rows.len(), 3);
        assert_eq!(rows[0].depth, 0);
        assert_eq!(rows[1].depth, 1);
        assert_eq!(rows[2].depth, 2);
        assert_eq!(rows[2].name, "Leaf");
    }

    #[test]
    fn multiple_roots_ordered() {
        let a = test_lib(1, "Alpha", None);
        let b = test_lib(2, "Beta", None);
        let rows = flatten_tree(&[a, b], &Default::default(), &Default::default(), 0);
        assert_eq!(rows.len(), 2);
        assert_eq!(rows[0].name, "Alpha");
        assert_eq!(rows[1].name, "Beta");
    }

    #[test]
    fn only_expanded_subtrees_included() {
        // Parent expanded, but sibling collapsed
        let p = test_lib(1, "P", None);
        let s = test_lib(2, "S", None); // sibling root, also collapsed
        let c = test_lib(3, "C", Some(1));
        let sc = test_lib(4, "SC", Some(2));
        let mut cache = HashMap::new();
        cache.insert(id_str(1), vec![c]);
        cache.insert(id_str(2), vec![sc]);

        let mut expanded = HashSet::new();
        expanded.insert(id_str(1)); // expand only P

        let rows = flatten_tree(&[p, s], &cache, &expanded, 0);
        assert_eq!(rows.len(), 3); // P, C, S (SC hidden)
        assert_eq!(rows[0].name, "P");
        assert_eq!(rows[1].name, "C");
        assert_eq!(rows[2].name, "S");
    }
}
