//! The Library tab: three-pane layout with tree, list, and detail panes.

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

use brittle_keymap::actions;
use leptos::prelude::*;
use leptos::task::spawn_local;

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

use crate::{
    command_bar::SearchQuery,
    lib_tree::{flatten_tree, LibraryTree, TreeRow},
    pub_detail::PubDetail,
    pub_list::PubList,
    ActionEvent,
};

// ── Pane enum ─────────────────────────────────────────────────────────────────

/// Which of the three panes currently has keyboard focus.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Pane {
    Tree,
    List,
    Detail,
}

impl Pane {
    pub fn next(self) -> Self {
        match self {
            Pane::Tree => Pane::List,
            Pane::List => Pane::Detail,
            Pane::Detail => Pane::Tree,
        }
    }

    pub fn prev(self) -> Self {
        match self {
            Pane::Tree => Pane::Detail,
            Pane::List => Pane::Tree,
            Pane::Detail => Pane::List,
        }
    }
}

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

/// Root component for the Library tab.
///
/// Owns all UI state, wires keymap actions, and drives async data loading.
#[component]
pub fn LibTab() -> impl IntoView {
    // ── Context signals ───────────────────────────────────────────────────────
    let keymap_action = use_context::<crate::KeymapAction>()
        .expect("KeymapAction context missing")
        .0;

    let search_query = use_context::<SearchQuery>()
        .map(|sq| sq.0)
        .unwrap_or_else(|| RwSignal::new(String::new()));

    let layout_config = use_context::<crate::LayoutContext>()
        .expect("LayoutContext missing")
        .0;

    // ── UI state ──────────────────────────────────────────────────────────────
    let focused = RwSignal::new(Pane::Tree);

    // Resizing state
    let left_width = RwSignal::new(220);
    let right_width = RwSignal::new(300);
    let resizing_left = RwSignal::new(false);
    let resizing_right = RwSignal::new(false);

    // Initial widths from layout config (fractions)
    Effect::new(move |_| {
        let config = layout_config.get();
        if let Some(win) = web_sys::window() {
            if let Ok(Some(total_width)) = win.inner_width().map(|v| v.as_f64()) {
                left_width.set((total_width * config.left_pane_fraction as f64) as i32);
                right_width.set((total_width * config.right_pane_fraction as f64) as i32);
            }
        }
    });

    // Tree state
    let root_libs = RwSignal::new(Vec::<Library>::new());
    let children_cache = RwSignal::new(HashMap::<String, Vec<Library>>::new());
    let expanded = RwSignal::new(HashSet::<String>::new());
    let tree_cursor = RwSignal::new(0usize);

    // List state
    let list_items = RwSignal::new(Vec::<ReferenceSummary>::new());
    let list_cursor = RwSignal::new(0usize);

    // Detail state
    let detail_ref = RwSignal::new(Option::<Reference>::None);

    // ── Resizing Logic ────────────────────────────────────────────────────────
    let on_mousemove = window_event_listener(leptos::ev::mousemove, move |ev| {
        if let Some(win) = web_sys::window() {
            if let Ok(Some(total_width)) = win.inner_width().map(|v| v.as_f64()) {
                let total_width = total_width as i32;
                let config = layout_config.get_untracked();

                if resizing_left.get_untracked() {
                    let new_width = ev.client_x();
                    // Constraints: min, max, AND leave enough space for center
                    let max_allowed = (total_width - right_width.get_untracked() - config.center_pane_min).max(config.left_pane_min);
                    left_width.set(new_width.max(config.left_pane_min).min(config.left_pane_max).min(max_allowed));
                } else if resizing_right.get_untracked() {
                    let new_width = total_width - ev.client_x();
                    // Constraints: min, max, AND leave enough space for center
                    let max_allowed = (total_width - left_width.get_untracked() - config.center_pane_min).max(config.right_pane_min);
                    right_width.set(new_width.max(config.right_pane_min).min(config.right_pane_max).min(max_allowed));
                }
            }
        }
    });

    let on_mouseup = window_event_listener(leptos::ev::mouseup, move |_| {
        resizing_left.set(false);
        resizing_right.set(false);
    });

    on_cleanup(move || {
        on_mousemove.remove();
        on_mouseup.remove();
    });

    // ── Derived / computed ────────────────────────────────────────────────────

    // Flattened visible tree rows (recomputed when tree data or expand set changes).
    let tree_rows = Memo::new(move |_| {
        flatten_tree(&root_libs.get(), &children_cache.get(), &expanded.get(), 0)
    });

    // The library selected by the tree cursor.
    let selected_library = Memo::new(move |_| {
        let pos = tree_cursor.get();
        tree_rows.with(|rows| {
            rows.get(pos).and_then(|r| {
                uuid::Uuid::parse_str(&r.id).ok().map(LibraryId)
            })
        })
    });

    // The reference ID selected by the list cursor.
    let selected_ref_id = Memo::new(move |_| {
        let pos = list_cursor.get();
        list_items.with(|items| items.get(pos).map(|r| r.id.clone()))
    });

    // ── Initial data load (and reload on repository change) ───────────────────
    let reload_trigger = use_context::<crate::ReloadTrigger>().map(|r| r.0);
    Effect::new(move |_| {
        let trigger = reload_trigger.map(|t| t.get()).unwrap_or(0);
        if trigger == 0 { return; } // no repository open yet
        spawn_local(async move {
            match crate::tauri::list_root_libraries().await {
                Ok(libs) => root_libs.set(libs),
                Err(e) => leptos::logging::log!("load libraries: {e}"),
            }
        });
    });

    // ── Reactive data loads ───────────────────────────────────────────────────

    // Reload list whenever the selected library or search query changes.
    Effect::new(move |_| {
        let lib_id = selected_library.get();
        let query = search_query.get();
        let trigger = reload_trigger.map(|t| t.get_untracked()).unwrap_or(0);
        if trigger == 0 { return; } // no repository open yet
        spawn_local(async move {
            let result = match (&lib_id, query.is_empty()) {
                (Some(id), true) => crate::tauri::list_library_references_recursive(id).await,
                (Some(id), false) => crate::tauri::search_library_references(id, &query).await,
                (None, true) => crate::tauri::list_references().await,
                (None, false) => crate::tauri::search_references(&query).await,
            };
            match result {
                Ok(items) => list_items.set(items),
                Err(e) => {
                    leptos::logging::error!("load publications: {e}");
                    list_items.set(vec![]);
                }
            }
            list_cursor.set(0);
        });
    });

    // Load full reference when list cursor moves.
    Effect::new(move |_| {
        let ref_id: Option<ReferenceId> = selected_ref_id.get();
        spawn_local(async move {
            let loaded = match ref_id {
                Some(id) => crate::tauri::get_reference(&id).await.ok(),
                None => None,
            };
            detail_ref.set(loaded);
        });
    });

    // ── Keymap wiring ─────────────────────────────────────────────────────────
    Effect::new(move |_| {
        let Some(ev) = keymap_action.get() else { return };

        // ACTION_OPEN in the List or Detail pane opens a PDF tab for the selected reference.
        if ev.name == actions::ACTION_OPEN
            && matches!(focused.get_untracked(), Pane::List | Pane::Detail)
        {
            if let Some(ref_id) = selected_ref_id.get_untracked() {
                let title = detail_ref.with_untracked(|r| {
                    r.as_ref()
                        .map(|r| r.cite_key.clone())
                        .unwrap_or_else(|| ref_id.to_string())
                });
                if let Some(ctx) = use_context::<crate::OpenPdfContext>() {
                    ctx.0.set(Some(crate::PdfOpenRequest {
                        ref_id: ref_id.to_string(),
                        title,
                    }));
                }
            }
            return;
        }

        handle_action(
            &ev,
            focused,
            TreeState { cursor: tree_cursor, rows: tree_rows, expanded },
            ListState { cursor: list_cursor, items: list_items },
        );
    });

    // ── View ──────────────────────────────────────────────────────────────────
    view! {
        <div class="lib-tab">
            <div
                class="pane pane-left"
                style=move || format!("width: {}px", left_width.get())
            >
                <LibraryTree
                    root_libs=root_libs
                    children_cache=children_cache
                    expanded=expanded
                    cursor=tree_cursor
                    focused=focused
                />
            </div>

            <div
                class="resizer"
                class:resizing=move || resizing_left.get()
                on:mousedown=move |ev| {
                    ev.prevent_default();
                    resizing_left.set(true);
                }
            />

            <div class="pane pane-center">
                <PubList
                    items=list_items
                    cursor=list_cursor
                    focused=focused
                />
            </div>

            <div
                class="resizer"
                class:resizing=move || resizing_right.get()
                on:mousedown=move |ev| {
                    ev.prevent_default();
                    resizing_right.set(true);
                }
            />

            <div
                class="pane pane-right"
                class:pane-focused=move || focused.get() == Pane::Detail
                style=move || format!("width: {}px", right_width.get())
            >
                <PubDetail reference=detail_ref />
            </div>
        </div>
    }
}

// ── Action handler ────────────────────────────────────────────────────────────

struct TreeState {
    cursor: RwSignal<usize>,
    rows: Memo<Vec<TreeRow>>,
    expanded: RwSignal<HashSet<String>>,
}

struct ListState {
    cursor: RwSignal<usize>,
    items: RwSignal<Vec<ReferenceSummary>>,
}

fn handle_action(
    ev: &ActionEvent,
    focused: RwSignal<Pane>,
    tree: TreeState,
    list: ListState,
) {
    let count = (ev.count as usize).max(1);
    let cur = focused.get_untracked();

    match ev.name.as_str() {
        // ── Focus switching ───────────────────────────────────────────────────
        actions::FOCUS_LEFT => focused.set(Pane::Tree),
        actions::FOCUS_CENTER => focused.set(Pane::List),
        actions::FOCUS_RIGHT => focused.set(Pane::Detail),
        actions::FOCUS_NEXT => focused.update(|p| *p = p.next()),
        actions::FOCUS_PREV => focused.update(|p| *p = p.prev()),

        // ── Tree navigation ───────────────────────────────────────────────────
        actions::NAV_DOWN if cur == Pane::Tree => {
            let len = tree.rows.with_untracked(Vec::len);
            if len > 0 {
                tree.cursor.update(|c| *c = (*c + count).min(len - 1));
            }
        }
        actions::NAV_UP if cur == Pane::Tree => {
            tree.cursor.update(|c| *c = c.saturating_sub(count));
        }
        actions::NAV_TOP if cur == Pane::Tree => tree.cursor.set(0),
        actions::NAV_BOTTOM if cur == Pane::Tree => {
            let len = tree.rows.with_untracked(Vec::len);
            if len > 0 {
                tree.cursor.set(len - 1);
            }
        }
        actions::NAV_PAGE_DOWN if cur == Pane::Tree => {
            let len = tree.rows.with_untracked(Vec::len);
            if len > 0 {
                tree.cursor.update(|c| *c = (*c + 10 * count).min(len - 1));
            }
        }
        actions::NAV_PAGE_UP if cur == Pane::Tree => {
            tree.cursor.update(|c| *c = c.saturating_sub(10 * count));
        }

        // ── Tree expand / collapse ────────────────────────────────────────────
        n if (n == actions::TREE_EXPAND || n == actions::ACTION_OPEN) && cur == Pane::Tree => {
            tree_expand(tree.cursor, tree.rows, tree.expanded);
        }
        actions::TREE_COLLAPSE if cur == Pane::Tree => {
            tree_collapse(tree.cursor, tree.rows, tree.expanded);
        }
        actions::TREE_TOGGLE if cur == Pane::Tree => {
            let id = tree.rows.with_untracked(|rows| {
                rows.get(tree.cursor.get_untracked()).map(|r| r.id.clone())
            });
            if let Some(id) = id {
                let is_open = tree.expanded.with_untracked(|e| e.contains(&id));
                if is_open {
                    tree.expanded.update(|e| { e.remove(&id); });
                } else {
                    tree.expanded.update(|e| { e.insert(id.clone()); });
                    // Child loading is handled reactively by LibraryTree's Effect.
                }
            }
        }

        // ── List navigation ───────────────────────────────────────────────────
        actions::NAV_DOWN if cur == Pane::List => {
            let len = list.items.with_untracked(Vec::len);
            if len > 0 {
                list.cursor.update(|c| *c = (*c + count).min(len - 1));
            }
        }
        actions::NAV_UP if cur == Pane::List => {
            list.cursor.update(|c| *c = c.saturating_sub(count));
        }
        actions::NAV_TOP if cur == Pane::List => list.cursor.set(0),
        actions::NAV_BOTTOM if cur == Pane::List => {
            let len = list.items.with_untracked(Vec::len);
            if len > 0 {
                list.cursor.set(len - 1);
            }
        }
        actions::NAV_PAGE_DOWN if cur == Pane::List => {
            let len = list.items.with_untracked(Vec::len);
            if len > 0 {
                list.cursor.update(|c| *c = (*c + 10 * count).min(len - 1));
            }
        }
        actions::NAV_PAGE_UP if cur == Pane::List => {
            list.cursor.update(|c| *c = c.saturating_sub(10 * count));
        }

        _ => {}
    }
}

// ── Tree helpers ──────────────────────────────────────────────────────────────

fn tree_expand(
    cursor: RwSignal<usize>,
    rows: Memo<Vec<TreeRow>>,
    expanded: RwSignal<HashSet<String>>,
) {
    let id = rows.with_untracked(|r| r.get(cursor.get_untracked()).map(|row| row.id.clone()));
    if let Some(id) = id {
        expanded.update(|e| { e.insert(id.clone()); });
        // Child loading is handled reactively by LibraryTree's Effect.
    }
}

fn tree_collapse(
    cursor: RwSignal<usize>,
    rows: Memo<Vec<TreeRow>>,
    expanded: RwSignal<HashSet<String>>,
) {
    let id = rows.with_untracked(|r| r.get(cursor.get_untracked()).map(|row| row.id.clone()));
    if let Some(id) = id {
        expanded.update(|e| { e.remove(&id); });
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use brittle_keymap::actions;
    use leptos::prelude::*;

    fn ev(name: &str) -> ActionEvent {
        ActionEvent { name: name.to_owned(), count: 1, seq: 0 }
    }

    fn ev_count(name: &str, count: u32) -> ActionEvent {
        ActionEvent { name: name.to_owned(), count, seq: 0 }
    }

    fn make_tree_rows(n: usize) -> Vec<TreeRow> {
        (0..n)
            .map(|i| TreeRow {
                id: format!("row-{i}"),
                name: format!("Row {i}"),
                depth: 0,
                expanded: false,
                may_have_children: false,
            })
            .collect()
    }

    fn make_ref_summaries(n: usize) -> Vec<ReferenceSummary> {
        use brittle_model::{EntryType, ReferenceId};
        (0..n)
            .map(|_| ReferenceSummary {
                id: ReferenceId::new(),
                cite_key: "x".into(),
                entry_type: EntryType::Misc,
                title: None,
                authors: vec![],
                year: None,
            })
            .collect()
    }

    // Set up signals and call handle_action in a Leptos reactive owner.
    // Returns the signal values after the action.
    fn run<F>(focused_init: Pane, tree_len: usize, list_len: usize, ev: &ActionEvent, check: F)
    where
        F: FnOnce(RwSignal<Pane>, RwSignal<usize>, RwSignal<usize>, RwSignal<HashSet<String>>),
    {
        let owner = Owner::new();
        owner.with(|| {
            let focused = RwSignal::new(focused_init);
            let tree_cursor = RwSignal::new(0usize);
            let list_cursor = RwSignal::new(0usize);
            let expanded = RwSignal::new(HashSet::<String>::new());
            let rows_data = make_tree_rows(tree_len);
            let rows = Memo::new(move |_| rows_data.clone());
            let list_items = RwSignal::new(make_ref_summaries(list_len));

            handle_action(
                ev,
                focused,
                TreeState { cursor: tree_cursor, rows, expanded },
                ListState { cursor: list_cursor, items: list_items },
            );

            check(focused, tree_cursor, list_cursor, expanded);
        });
    }

    // ── Focus switching ────────────────────────────────────────────────────────

    #[test]
    fn focus_left_sets_tree() {
        run(Pane::List, 0, 0, &ev(actions::FOCUS_LEFT), |f, _, _, _| {
            assert_eq!(f.get_untracked(), Pane::Tree);
        });
    }

    #[test]
    fn focus_center_sets_list() {
        run(Pane::Tree, 0, 0, &ev(actions::FOCUS_CENTER), |f, _, _, _| {
            assert_eq!(f.get_untracked(), Pane::List);
        });
    }

    #[test]
    fn focus_right_sets_detail() {
        run(Pane::Tree, 0, 0, &ev(actions::FOCUS_RIGHT), |f, _, _, _| {
            assert_eq!(f.get_untracked(), Pane::Detail);
        });
    }

    #[test]
    fn focus_next_cycles_forward() {
        run(Pane::Tree, 0, 0, &ev(actions::FOCUS_NEXT), |f, _, _, _| {
            assert_eq!(f.get_untracked(), Pane::List);
        });
        run(Pane::List, 0, 0, &ev(actions::FOCUS_NEXT), |f, _, _, _| {
            assert_eq!(f.get_untracked(), Pane::Detail);
        });
        run(Pane::Detail, 0, 0, &ev(actions::FOCUS_NEXT), |f, _, _, _| {
            assert_eq!(f.get_untracked(), Pane::Tree);
        });
    }

    #[test]
    fn focus_prev_cycles_backward() {
        run(Pane::Tree, 0, 0, &ev(actions::FOCUS_PREV), |f, _, _, _| {
            assert_eq!(f.get_untracked(), Pane::Detail);
        });
    }

    // ── Tree navigation ────────────────────────────────────────────────────────

    #[test]
    fn nav_down_advances_tree_cursor() {
        run(Pane::Tree, 5, 0, &ev(actions::NAV_DOWN), |_, tc, _, _| {
            assert_eq!(tc.get_untracked(), 1);
        });
    }

    #[test]
    fn nav_down_clamps_at_tree_end() {
        let owner = Owner::new();
        owner.with(|| {
            let focused = RwSignal::new(Pane::Tree);
            let tree_cursor = RwSignal::new(4usize); // already at last row
            let expanded = RwSignal::new(HashSet::<String>::new());
            let rows_data = make_tree_rows(5);
            let rows = Memo::new(move |_| rows_data.clone());
            let list_cursor = RwSignal::new(0usize);
            let list_items = RwSignal::new(vec![]);

            handle_action(
                &ev(actions::NAV_DOWN),
                focused,
                TreeState { cursor: tree_cursor, rows, expanded },
                ListState { cursor: list_cursor, items: list_items },
            );

            assert_eq!(tree_cursor.get_untracked(), 4); // stayed at 4
        });
    }

    #[test]
    fn nav_up_decrements_tree_cursor() {
        let owner = Owner::new();
        owner.with(|| {
            let focused = RwSignal::new(Pane::Tree);
            let tree_cursor = RwSignal::new(3usize);
            let expanded = RwSignal::new(HashSet::<String>::new());
            let rows_data = make_tree_rows(5);
            let rows = Memo::new(move |_| rows_data.clone());
            handle_action(
                &ev(actions::NAV_UP),
                focused,
                TreeState { cursor: tree_cursor, rows, expanded },
                ListState { cursor: RwSignal::new(0), items: RwSignal::new(vec![]) },
            );
            assert_eq!(tree_cursor.get_untracked(), 2);
        });
    }

    #[test]
    fn nav_up_clamps_at_zero() {
        run(Pane::Tree, 5, 0, &ev(actions::NAV_UP), |_, tc, _, _| {
            assert_eq!(tc.get_untracked(), 0); // already 0, stays 0
        });
    }

    #[test]
    fn nav_top_jumps_tree_to_start() {
        let owner = Owner::new();
        owner.with(|| {
            let focused = RwSignal::new(Pane::Tree);
            let tree_cursor = RwSignal::new(4usize);
            let expanded = RwSignal::new(HashSet::<String>::new());
            let rows_data = make_tree_rows(5);
            let rows = Memo::new(move |_| rows_data.clone());
            handle_action(
                &ev(actions::NAV_TOP),
                focused,
                TreeState { cursor: tree_cursor, rows, expanded },
                ListState { cursor: RwSignal::new(0), items: RwSignal::new(vec![]) },
            );
            assert_eq!(tree_cursor.get_untracked(), 0);
        });
    }

    #[test]
    fn nav_bottom_jumps_tree_to_end() {
        run(Pane::Tree, 5, 0, &ev(actions::NAV_BOTTOM), |_, tc, _, _| {
            assert_eq!(tc.get_untracked(), 4);
        });
    }

    #[test]
    fn nav_down_with_count_advances_multiple() {
        run(Pane::Tree, 10, 0, &ev_count(actions::NAV_DOWN, 3), |_, tc, _, _| {
            assert_eq!(tc.get_untracked(), 3);
        });
    }

    // ── List navigation ────────────────────────────────────────────────────────

    #[test]
    fn nav_down_advances_list_cursor_when_focused() {
        run(Pane::List, 0, 5, &ev(actions::NAV_DOWN), |_, _, lc, _| {
            assert_eq!(lc.get_untracked(), 1);
        });
    }

    #[test]
    fn nav_down_on_tree_does_not_advance_list_cursor() {
        run(Pane::Tree, 3, 3, &ev(actions::NAV_DOWN), |_, tc, lc, _| {
            assert_eq!(tc.get_untracked(), 1); // tree moved
            assert_eq!(lc.get_untracked(), 0); // list unchanged
        });
    }

    #[test]
    fn nav_bottom_jumps_list_to_last_item() {
        run(Pane::List, 0, 5, &ev(actions::NAV_BOTTOM), |_, _, lc, _| {
            assert_eq!(lc.get_untracked(), 4);
        });
    }

    // ── Tree expand/collapse ───────────────────────────────────────────────────

    #[test]
    fn tree_expand_action_inserts_id_into_expanded() {
        run(Pane::Tree, 3, 0, &ev(actions::TREE_EXPAND), |_, _, _, exp| {
            assert!(exp.with_untracked(|e| e.contains("row-0")));
        });
    }

    #[test]
    fn tree_collapse_action_removes_id_from_expanded() {
        let owner = Owner::new();
        owner.with(|| {
            let focused = RwSignal::new(Pane::Tree);
            let tree_cursor = RwSignal::new(0usize);
            let expanded = RwSignal::new(HashSet::from_iter(["row-0".to_string()]));
            let rows_data = make_tree_rows(3);
            let rows = Memo::new(move |_| rows_data.clone());
            handle_action(
                &ev(actions::TREE_COLLAPSE),
                focused,
                TreeState { cursor: tree_cursor, rows, expanded },
                ListState { cursor: RwSignal::new(0), items: RwSignal::new(vec![]) },
            );
            assert!(!expanded.with_untracked(|e| e.contains("row-0")));
        });
    }

    #[test]
    fn tree_actions_are_no_ops_when_list_focused() {
        run(Pane::List, 3, 0, &ev(actions::TREE_EXPAND), |_, _, _, exp| {
            assert!(exp.with_untracked(|e| e.is_empty()));
        });
    }
}
