use serde::Serialize;
use std::collections::BTreeSet;

pub const PICKER_STATE_REDUCER_CONTRACT_NAME: &str = "rust_picker_state_reducer_v1";

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

impl Default for PickerStateReducerContract {
    fn default() -> Self {
        Self {
            contract_name: PICKER_STATE_REDUCER_CONTRACT_NAME,
            actions: BTreeSet::from([
                "normalize",
                "focus",
                "next",
                "previous",
                "home",
                "end",
                "pick",
            ]),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PickerItem {
    pub item_key: String,
    pub document_id: Option<String>,
    pub pickable: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PickerState {
    pub active_item_key: Option<String>,
}

impl PickerState {
    pub fn normalize(&self, items: &[PickerItem], excluded_ids: &BTreeSet<String>) -> Self {
        if let Some(active_item_key) = self.active_item_key.as_deref() {
            if is_pickable_item_key(items, excluded_ids, active_item_key) {
                return self.clone();
            }
        }
        Self {
            active_item_key: first_pickable_item_key(items, excluded_ids),
        }
    }

    pub fn move_next(&self, items: &[PickerItem], excluded_ids: &BTreeSet<String>) -> Self {
        self.move_by(items, excluded_ids, 1)
    }

    pub fn move_previous(&self, items: &[PickerItem], excluded_ids: &BTreeSet<String>) -> Self {
        self.move_by(items, excluded_ids, -1)
    }

    pub fn move_home(&self, items: &[PickerItem], excluded_ids: &BTreeSet<String>) -> Self {
        Self {
            active_item_key: first_pickable_item_key(items, excluded_ids),
        }
    }

    pub fn move_end(&self, items: &[PickerItem], excluded_ids: &BTreeSet<String>) -> Self {
        Self {
            active_item_key: pickable_items(items, excluded_ids)
                .last()
                .map(|item| item.item_key.clone()),
        }
    }

    pub fn pick(&self, items: &[PickerItem], excluded_ids: &BTreeSet<String>) -> Option<String> {
        let active = self.active_item_key.as_deref()?;
        pickable_items(items, excluded_ids)
            .find(|item| item.item_key == active)
            .and_then(|item| item.document_id.clone())
    }

    fn move_by(
        &self,
        items: &[PickerItem],
        excluded_ids: &BTreeSet<String>,
        offset: isize,
    ) -> Self {
        let pickable = pickable_items(items, excluded_ids).collect::<Vec<_>>();
        if pickable.is_empty() {
            return Self::default();
        }
        let current_index = self
            .active_item_key
            .as_deref()
            .and_then(|active| pickable.iter().position(|item| item.item_key == active))
            .unwrap_or(0);
        let next_index =
            (current_index as isize + offset).clamp(0, (pickable.len() - 1) as isize) as usize;
        Self {
            active_item_key: Some(pickable[next_index].item_key.clone()),
        }
    }
}

fn is_item_excluded(item: &PickerItem, excluded_ids: &BTreeSet<String>) -> bool {
    item.document_id
        .as_ref()
        .is_some_and(|document_id| excluded_ids.contains(document_id))
}

fn pickable_items<'a>(
    items: &'a [PickerItem],
    excluded_ids: &'a BTreeSet<String>,
) -> impl DoubleEndedIterator<Item = &'a PickerItem> + 'a {
    items
        .iter()
        .filter(move |item| item.pickable && !is_item_excluded(item, excluded_ids))
}

fn first_pickable_item_key(
    items: &[PickerItem],
    excluded_ids: &BTreeSet<String>,
) -> Option<String> {
    pickable_items(items, excluded_ids)
        .next()
        .map(|item| item.item_key.clone())
}

fn is_pickable_item_key(
    items: &[PickerItem],
    excluded_ids: &BTreeSet<String>,
    item_key: &str,
) -> bool {
    pickable_items(items, excluded_ids).any(|item| item.item_key == item_key)
}

#[cfg(test)]
mod tests {
    use super::{
        PickerItem, PickerState, PickerStateReducerContract, PICKER_STATE_REDUCER_CONTRACT_NAME,
    };
    use std::collections::BTreeSet;

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

    fn item(item_key: &str, document_id: Option<&str>, pickable: bool) -> PickerItem {
        PickerItem {
            item_key: item_key.into(),
            document_id: document_id.map(ToOwned::to_owned),
            pickable,
        }
    }

    #[test]
    fn tree_shell_picker_state_skips_excluded_items_and_picks_active_document() {
        let items = vec![
            item("root", None, true),
            item("doc:a", Some("doc:a"), true),
            item("doc:b", Some("doc:b"), true),
            item("doc:c", Some("doc:c"), true),
        ];
        let excluded_ids = excluded(&["doc:b"]);

        let state = PickerState {
            active_item_key: Some("doc:b".into()),
        }
        .normalize(&items, &excluded_ids);
        assert_eq!(state.active_item_key.as_deref(), Some("root"));

        let state = state
            .move_next(&items, &excluded_ids)
            .move_next(&items, &excluded_ids);
        assert_eq!(state.active_item_key.as_deref(), Some("doc:c"));
        assert_eq!(state.pick(&items, &excluded_ids).as_deref(), Some("doc:c"));

        let state = state.move_previous(&items, &excluded_ids);
        assert_eq!(state.active_item_key.as_deref(), Some("doc:a"));
    }

    #[test]
    fn tree_shell_picker_state_home_end_and_empty_cases_are_stable() {
        let items = vec![
            item("doc:a", Some("doc:a"), true),
            item("doc:b", Some("doc:b"), false),
            item("doc:c", Some("doc:c"), true),
        ];
        let excluded_ids = excluded(&[]);
        let state = PickerState::default().move_end(&items, &excluded_ids);
        assert_eq!(state.active_item_key.as_deref(), Some("doc:c"));
        assert_eq!(
            state
                .move_home(&items, &excluded_ids)
                .active_item_key
                .as_deref(),
            Some("doc:a")
        );

        let empty = PickerState::default().normalize(&items, &excluded(&["doc:a", "doc:c"]));
        assert_eq!(empty.active_item_key, None);
        assert_eq!(empty.pick(&items, &excluded(&["doc:a", "doc:c"])), None);
    }

    #[test]
    fn tree_shell_picker_state_reducer_contract_exposes_supported_actions() {
        let contract = PickerStateReducerContract::default();
        assert_eq!(contract.contract_name, PICKER_STATE_REDUCER_CONTRACT_NAME);
        assert!(contract.actions.contains("normalize"));
        assert!(contract.actions.contains("focus"));
        assert!(contract.actions.contains("next"));
        assert!(contract.actions.contains("previous"));
        assert!(contract.actions.contains("home"));
        assert!(contract.actions.contains("end"));
        assert!(contract.actions.contains("pick"));
    }
}
