use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

pub const FILETREE_SELECTION_REDUCER_CONTRACT_NAME: &str = "rust_filetree_selection_reducer_v1";

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileTreeSelectionReducerContract {
    pub contract_name: &'static str,
    pub actions: BTreeSet<&'static str>,
}

impl Default for FileTreeSelectionReducerContract {
    fn default() -> Self {
        Self {
            contract_name: FILETREE_SELECTION_REDUCER_CONTRACT_NAME,
            actions: BTreeSet::from([
                "select_row",
                "select_context_row",
                "normalize_visible_rows",
                "clear",
                "resolve_drag_rows",
            ]),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileTreeSelectionModifiers {
    pub shift_key: bool,
    pub ctrl_key: bool,
    pub meta_key: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileTreeSelectionState {
    pub selected_row_ids: BTreeSet<String>,
    pub anchor_row_id: Option<String>,
    pub focused_row_id: Option<String>,
}

impl FileTreeSelectionState {
    pub fn from_selected(row_ids: &[String]) -> Self {
        Self {
            selected_row_ids: row_ids.iter().cloned().collect(),
            anchor_row_id: row_ids.first().cloned(),
            focused_row_id: row_ids.first().cloned(),
        }
    }

    pub fn select_row(
        &self,
        row_id: &str,
        visible_row_ids: &[String],
        modifiers: FileTreeSelectionModifiers,
    ) -> Self {
        let toggle_selection = modifiers.ctrl_key || modifiers.meta_key;

        if modifiers.shift_key {
            let anchor = self
                .anchor_row_id
                .clone()
                .or_else(|| self.focused_row_id.clone())
                .unwrap_or_else(|| row_id.to_string());
            let mut selected_row_ids = if toggle_selection {
                self.selected_row_ids.clone()
            } else {
                BTreeSet::new()
            };
            for range_row_id in range_row_ids(visible_row_ids, &anchor, row_id) {
                selected_row_ids.insert(range_row_id);
            }
            return Self {
                selected_row_ids,
                anchor_row_id: self.anchor_row_id.clone().or(Some(anchor)),
                focused_row_id: Some(row_id.to_string()),
            };
        }

        if toggle_selection {
            let mut selected_row_ids = self.selected_row_ids.clone();
            if selected_row_ids.contains(row_id) {
                selected_row_ids.remove(row_id);
            } else {
                selected_row_ids.insert(row_id.to_string());
            }
            return Self {
                selected_row_ids,
                anchor_row_id: Some(row_id.to_string()),
                focused_row_id: Some(row_id.to_string()),
            };
        }

        Self {
            selected_row_ids: BTreeSet::from([row_id.to_string()]),
            anchor_row_id: Some(row_id.to_string()),
            focused_row_id: Some(row_id.to_string()),
        }
    }

    pub fn select_context_row(&self, row_id: &str) -> Self {
        if self.selected_row_ids.contains(row_id) {
            return Self {
                selected_row_ids: self.selected_row_ids.clone(),
                anchor_row_id: self.anchor_row_id.clone(),
                focused_row_id: Some(row_id.to_string()),
            };
        }

        Self {
            selected_row_ids: BTreeSet::from([row_id.to_string()]),
            anchor_row_id: Some(row_id.to_string()),
            focused_row_id: Some(row_id.to_string()),
        }
    }

    pub fn normalize_for_visible_rows(&self, visible_row_ids: &[String]) -> Self {
        let visible = visible_row_ids.iter().collect::<BTreeSet<_>>();
        Self {
            selected_row_ids: self
                .selected_row_ids
                .iter()
                .filter(|row_id| visible.contains(row_id))
                .cloned()
                .collect(),
            anchor_row_id: self
                .anchor_row_id
                .as_ref()
                .filter(|row_id| visible.contains(row_id))
                .cloned(),
            focused_row_id: self
                .focused_row_id
                .as_ref()
                .filter(|row_id| visible.contains(row_id))
                .cloned(),
        }
    }

    pub fn clear(&self) -> Self {
        Self::default()
    }

    pub fn resolve_drag_row_ids(&self, row_id: &str) -> Vec<String> {
        if self.selected_row_ids.contains(row_id) {
            return self.selected_row_ids.iter().cloned().collect();
        }
        vec![row_id.to_string()]
    }

    pub fn resolve_drag_row_ids_for_visible_rows(
        &self,
        row_id: &str,
        visible_row_ids: &[String],
    ) -> Vec<String> {
        if !self.selected_row_ids.contains(row_id) {
            return vec![row_id.to_string()];
        }
        let ordered = visible_row_ids
            .iter()
            .filter(|visible_row_id| self.selected_row_ids.contains(*visible_row_id))
            .cloned()
            .collect::<Vec<_>>();
        if ordered.is_empty() {
            self.selected_row_ids.iter().cloned().collect()
        } else {
            ordered
        }
    }
}

fn range_row_ids(visible_row_ids: &[String], from_id: &str, to_id: &str) -> Vec<String> {
    let from_index = visible_row_ids.iter().position(|row_id| row_id == from_id);
    let to_index = visible_row_ids.iter().position(|row_id| row_id == to_id);
    let (Some(from_index), Some(to_index)) = (from_index, to_index) else {
        return vec![to_id.to_string()];
    };
    let low = from_index.min(to_index);
    let high = from_index.max(to_index);
    visible_row_ids[low..=high].to_vec()
}

#[cfg(test)]
mod tests {
    use super::{
        FileTreeSelectionModifiers, FileTreeSelectionReducerContract, FileTreeSelectionState,
        FILETREE_SELECTION_REDUCER_CONTRACT_NAME,
    };

    fn ids(values: &[&str]) -> Vec<String> {
        values.iter().map(|value| (*value).to_string()).collect()
    }

    #[test]
    fn tree_shell_filetree_selection_click_toggle_and_range_follow_contract() {
        let visible = ids(&["doc:a", "doc:b", "asset:c", "asset:d"]);
        let mut state = FileTreeSelectionState::default();

        state = state.select_row("doc:b", &visible, FileTreeSelectionModifiers::default());
        assert_eq!(
            state.selected_row_ids,
            ids(&["doc:b"]).into_iter().collect()
        );
        assert_eq!(state.anchor_row_id.as_deref(), Some("doc:b"));
        assert_eq!(state.focused_row_id.as_deref(), Some("doc:b"));

        state = state.select_row(
            "asset:d",
            &visible,
            FileTreeSelectionModifiers {
                shift_key: true,
                ..FileTreeSelectionModifiers::default()
            },
        );
        assert_eq!(
            state.selected_row_ids,
            ids(&["doc:b", "asset:c", "asset:d"]).into_iter().collect()
        );
        assert_eq!(state.anchor_row_id.as_deref(), Some("doc:b"));
        assert_eq!(state.focused_row_id.as_deref(), Some("asset:d"));

        state = state.select_row(
            "doc:a",
            &visible,
            FileTreeSelectionModifiers {
                ctrl_key: true,
                ..FileTreeSelectionModifiers::default()
            },
        );
        assert_eq!(
            state.selected_row_ids,
            ids(&["doc:a", "doc:b", "asset:c", "asset:d"])
                .into_iter()
                .collect()
        );
        assert_eq!(state.anchor_row_id.as_deref(), Some("doc:a"));
        assert_eq!(state.focused_row_id.as_deref(), Some("doc:a"));
    }

    #[test]
    fn tree_shell_filetree_selection_context_clear_normalize_and_drag_rows_are_stable() {
        let visible = ids(&["doc:a", "doc:b", "asset:c"]);
        let mut state = FileTreeSelectionState::from_selected(&ids(&["doc:a", "asset:missing"]));
        state.focused_row_id = Some("asset:missing".into());

        state = state.normalize_for_visible_rows(&visible);
        assert_eq!(
            state.selected_row_ids,
            ids(&["doc:a"]).into_iter().collect()
        );
        assert_eq!(state.anchor_row_id.as_deref(), Some("doc:a"));
        assert_eq!(state.focused_row_id, None);

        state = state.select_context_row("asset:c");
        assert_eq!(
            state.selected_row_ids,
            ids(&["asset:c"]).into_iter().collect()
        );
        assert_eq!(state.anchor_row_id.as_deref(), Some("asset:c"));
        assert_eq!(state.focused_row_id.as_deref(), Some("asset:c"));

        assert_eq!(state.resolve_drag_row_ids("asset:c"), ids(&["asset:c"]));
        assert_eq!(state.resolve_drag_row_ids("doc:b"), ids(&["doc:b"]));

        state = state.clear();
        assert!(state.selected_row_ids.is_empty());
        assert_eq!(state.anchor_row_id, None);
        assert_eq!(state.focused_row_id, None);
    }

    #[test]
    fn tree_shell_filetree_selection_reducer_contract_exposes_supported_actions() {
        let contract = FileTreeSelectionReducerContract::default();
        assert_eq!(
            contract.contract_name,
            FILETREE_SELECTION_REDUCER_CONTRACT_NAME
        );
        assert!(contract.actions.contains("select_row"));
        assert!(contract.actions.contains("select_context_row"));
        assert!(contract.actions.contains("normalize_visible_rows"));
        assert!(contract.actions.contains("clear"));
        assert!(contract.actions.contains("resolve_drag_rows"));
    }
}
