use std::{
    cmp::Ordering,
    path::{Path, PathBuf},
};

use basalt_core::obsidian::{Note, VaultEntry};
use ratatui::widgets::ListState;

use crate::config::Symbols;

use super::Item;

#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub enum Sort {
    #[default]
    Asc,
    Desc,
}

#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub enum Visibility {
    Hidden,
    #[default]
    Visible,
    FullWidth,
}

#[derive(Debug, Default, Clone, PartialEq)]
pub struct ExplorerState {
    pub(crate) title: String,
    pub(crate) selected_note: Option<Note>,
    pub(crate) selected_item_index: Option<usize>,
    pub(crate) selected_item_path: Option<PathBuf>,
    pub(crate) items: Vec<Item>,
    pub(crate) flat_items: Vec<(Item, usize)>,
    pub(crate) visibility: Visibility,
    pub(crate) active: bool,
    pub(crate) sort: Sort,
    pub(crate) list_state: ListState,

    pub(crate) symbols: Symbols,

    pub(crate) editing: bool,
}

/// Calculates the vertical offset of list items in rows.
///
/// When the selected item is near the end of the list and there aren't enough items
/// remaining to keep the selection vertically centered, we shift the offset to show
/// as many trailing items as possible instead of centering the selection.
///
/// This prevents empty lines from appearing at the bottom of the list when the
/// selection moves toward the end.
///
/// Without this check, you'd see output like:
/// ╭────────╮
/// │ 3 item │
/// │>4 item │
/// │ 5 item │
/// │        │
/// ╰────────╯
///
/// With this check, the list scrolls up to fill the remaining space:
/// ╭────────╮
/// │ 2 item │
/// │ 3 item │
/// │>4 item │
/// │ 5 item │
/// ╰────────╯
///
/// The goal is to avoid showing unnecessary blank rows and to maximize visible items.
fn calculate_offset(row: usize, items_count: usize, window_height: usize) -> usize {
    let half = window_height / 2;

    if row + half > items_count.saturating_sub(1) {
        items_count.saturating_sub(window_height)
    } else {
        row.saturating_sub(half)
    }
}

pub fn flatten(sort: Sort, depth: usize) -> impl Fn(&Item) -> Vec<(Item, usize)> {
    move |item| match item {
        Item::File { .. } => vec![(item.clone(), depth)],
        Item::Directory {
            expanded: true,
            items,
            ..
        } => [(item.clone(), depth)]
            .into_iter()
            .chain({
                let mut items = items.clone();
                items.sort_by(sort_items_by(sort));
                items
                    .iter()
                    .flat_map(flatten(sort, depth + 1))
                    .collect::<Vec<_>>()
            })
            .collect(),
        Item::Directory {
            expanded: false, ..
        } => [(item.clone(), depth)].to_vec(),
    }
}

fn sort_items_by(sort: Sort) -> impl Fn(&Item, &Item) -> Ordering {
    move |a, b| match (a.is_dir(), b.is_dir()) {
        (true, false) => Ordering::Less,
        (false, true) => Ordering::Greater,
        (true, true) => natord::compare(a.name(), b.name()),
        _ => {
            let a = a.name().to_lowercase();
            let b = b.name().to_lowercase();
            match sort {
                Sort::Asc => natord::compare(&a, &b),
                Sort::Desc => natord::compare(&b, &a),
            }
        }
    }
}

impl ExplorerState {
    pub fn new(title: &str, items: Vec<VaultEntry>, symbols: &Symbols) -> Self {
        let items: Vec<Item> = items.into_iter().map(|entry| entry.into()).collect();
        let sort = Sort::default();

        let mut state = ExplorerState {
            title: title.to_string(),
            sort,
            active: true,
            visibility: Visibility::Visible,
            selected_item_index: None,
            selected_item_path: None,
            selected_note: None,
            symbols: symbols.clone(),
            list_state: ListState::default().with_selected(Some(0)),
            ..Default::default()
        };

        state.flatten_with_items(&items);
        state
    }

    pub fn set_active(&mut self, active: bool) {
        self.active = active;
    }

    fn map_to_item(&self, depth: usize, entry: VaultEntry) -> Item {
        match entry {
            VaultEntry::Directory {
                name,
                path,
                entries,
            } => {
                let expanded = self
                    .flat_items
                    .iter()
                    .find_map(|(item, _)| match item {
                        Item::Directory {
                            path: item_path,
                            expanded,
                            ..
                        } if &path == item_path => Some(*expanded),
                        _ => None,
                    })
                    .unwrap_or(false);

                Item::Directory {
                    name,
                    path,
                    expanded,
                    depth,
                    items: entries
                        .into_iter()
                        .map(|entry| self.map_to_item(depth + 1, entry))
                        .collect(),
                }
            }
            VaultEntry::File(note) => Item::File { note, depth },
        }
    }

    pub fn with_entries(&mut self, entries: Vec<VaultEntry>, select: Option<PathBuf>) {
        let items: Vec<Item> = entries
            .into_iter()
            .map(|entry| self.map_to_item(0, entry))
            .collect();

        self.flatten_with_items(&items);

        if let Some(path) = select {
            if let Some(index) = self.flat_items.iter().position(|(item, _)| match item {
                Item::File { note, .. } => note.path() == path,
                Item::Directory { path: dir_path, .. } => dir_path == &path,
            }) {
                self.list_state.select(Some(index));
                self.selected_item_index = Some(index);
                self.selected_item_path = Some(path);
            }
        }
    }

    pub fn hide_pane(&mut self) {
        match self.visibility {
            Visibility::FullWidth => self.visibility = Visibility::Visible,
            Visibility::Visible => self.visibility = Visibility::Hidden,
            _ => {}
        }
    }

    pub fn expand_pane(&mut self) {
        match self.visibility {
            Visibility::Hidden => self.visibility = Visibility::Visible,
            Visibility::Visible => self.visibility = Visibility::FullWidth,
            _ => {}
        }
    }

    pub fn toggle(&mut self) {
        if self.is_open() {
            self.visibility = Visibility::Hidden;
        } else {
            self.visibility = Visibility::Visible;
        }
    }

    pub fn flatten_with_sort(&mut self, sort: Sort) {
        let mut items = self.items.clone();
        items.sort_by(sort_items_by(sort));

        self.flat_items = items.iter().flat_map(flatten(sort, 0)).collect();
        self.items = items;
        self.sort = sort;
    }

    pub fn flatten_with_items(&mut self, items: &[Item]) {
        let mut items = items.to_vec();
        items.sort_by(sort_items_by(self.sort));

        self.flat_items = items.iter().flat_map(flatten(self.sort, 0)).collect();
        self.items = items.to_vec();
    }

    pub fn sort(&mut self) {
        let sort = match self.sort {
            Sort::Asc => Sort::Desc,
            Sort::Desc => Sort::Asc,
        };

        self.flatten_with_sort(sort)
    }

    pub fn update_offset_mut(&mut self, window_height: usize) -> &Self {
        if !self.items.is_empty() {
            let idx = self.list_state.selected().unwrap_or_default();
            let items_count = self.items.len();

            let offset = calculate_offset(idx, items_count, window_height);

            let list_state = &mut self.list_state;
            *list_state.offset_mut() = offset;
        }

        self
    }

    fn toggle_item_in_tree(item: &Item, identifier: &Path, always_open: bool) -> Item {
        let item = item.clone();

        match item {
            Item::Directory {
                expanded,
                path,
                name,
                items,
                depth,
            } => {
                let expanded = if path == identifier {
                    if always_open {
                        true
                    } else {
                        !expanded
                    }
                } else {
                    expanded
                };

                Item::Directory {
                    name,
                    path,
                    expanded,
                    depth,
                    items: items
                        .iter()
                        .map(|child| Self::toggle_item_in_tree(child, identifier, always_open))
                        .collect(),
                }
            }
            _ => item,
        }
    }

    /// Opens the current item. Returns `Some(true)` when a note was selected, and `Some(false)`
    /// when a directory was toggled.
    pub fn open(&mut self) -> Option<bool> {
        let selected_item_index = self.list_state.selected()?;
        let current_item = self.flat_items.get(selected_item_index)?;

        match current_item {
            (Item::Directory { path, .. }, _) => {
                let items: Vec<Item> = self
                    .items
                    .iter()
                    .map(|item| Self::toggle_item_in_tree(item, path, true))
                    .collect();

                self.flatten_with_items(&items);
                Some(false)
            }
            (Item::File { note, .. }, _) => {
                self.selected_note = Some(note.clone());
                self.selected_item_index = Some(selected_item_index);
                self.selected_item_path = Some(note.path().to_path_buf());
                Some(true)
            }
        }
    }

    /// Selects the current item. Returns `true` when a note was selected,
    /// `false` when a directory was toggled or there is no current item.
    pub fn select(&mut self) -> bool {
        let Some(selected_item_index) = self.list_state.selected() else {
            return false;
        };

        let Some(current_item) = self.flat_items.get(selected_item_index) else {
            return false;
        };

        match current_item {
            (Item::Directory { path, .. }, _) => {
                let items: Vec<Item> = self
                    .items
                    .clone()
                    .iter()
                    .map(|item| Self::toggle_item_in_tree(item, path, false))
                    .collect();

                self.flatten_with_items(&items);
                false
            }
            (Item::File { note, .. }, _) => {
                self.selected_note = Some(note.clone());
                self.selected_item_index = Some(selected_item_index);
                self.selected_item_path = Some(note.path().to_path_buf());
                true
            }
        }
    }

    pub fn current_item(&self) -> Option<&Item> {
        let selected_item_index = self.list_state.selected()?;
        self.flat_items
            .get(selected_item_index)
            .map(|(item, _)| item)
    }

    pub fn selected_path(&self) -> Option<PathBuf> {
        self.selected_item_path.clone()
    }

    pub fn is_open(&self) -> bool {
        matches!(self.visibility, Visibility::Visible | Visibility::FullWidth)
    }

    pub fn next(&mut self, amount: usize) {
        let index = self.list_state.selected().map(|i| {
            i.saturating_add(amount)
                .min(self.flat_items.len().saturating_sub(1))
        });

        self.list_state.select(index);
    }

    pub fn previous(&mut self, amount: usize) {
        let index = self.list_state.selected().map(|i| i.saturating_sub(amount));

        self.list_state.select(index);
    }
}

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

    #[test]
    fn with_entries_preserves_nested_file_depth() {
        let mut state = ExplorerState::default();

        let entries = vec![VaultEntry::Directory {
            name: "dir".into(),
            path: PathBuf::from("dir"),
            entries: vec![VaultEntry::File(Note::new_unchecked(
                "nested",
                &PathBuf::from("dir/nested"),
            ))],
        }];

        state.with_entries(entries, None);

        let Item::Directory { items, depth, .. } = &state.items[0] else {
            panic!("expected directory");
        };
        assert_eq!(*depth, 0);
        assert_eq!(items[0].depth(), 1, "nested file should keep its depth");
    }
}
