use super::picker_state::{PickerItem, PickerState};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PickerRuntimeEnvironment {
    pub items: Vec<PickerRuntimeItem>,
    pub excluded_ids: BTreeSet<String>,
    pub allow_root_pick: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PickerRuntimeItem {
    pub item_key: String,
    pub document_id: Option<String>,
    pub pickable: bool,
}

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

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PickerRuntimeTransition {
    pub state: PickerRuntimeState,
    pub outputs: BTreeSet<PickerRuntimeOutput>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
    tag = "kind",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub enum PickerRuntimeAction {
    Normalize,
    Focus { item_key: String, focus_dom: bool },
    Next,
    Previous,
    Home,
    End,
    Pick,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PickerRuntimeOutput {
    DomPatch { focus_dom: bool },
    Pick(PickerRuntimePickTarget),
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(
    tag = "kind",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub enum PickerRuntimePickTarget {
    Root,
    Document { document_id: String },
}

impl PickerRuntimeState {
    pub fn reduce(
        &self,
        env: &PickerRuntimeEnvironment,
        action: PickerRuntimeAction,
    ) -> PickerRuntimeTransition {
        match action {
            PickerRuntimeAction::Normalize => self.with_picker_state(
                env,
                to_picker_state(self).normalize(&picker_items(env), &env.excluded_ids),
                false,
            ),
            PickerRuntimeAction::Focus {
                item_key,
                focus_dom,
            } => {
                if is_pickable_item_key(env, &item_key) {
                    let mut state = self.clone();
                    state.active_item_key = Some(item_key);
                    transition(state, [PickerRuntimeOutput::DomPatch { focus_dom }])
                } else {
                    transition(self.clone(), [])
                }
            }
            PickerRuntimeAction::Next => self.with_picker_state(
                env,
                to_picker_state(self).move_next(&picker_items(env), &env.excluded_ids),
                false,
            ),
            PickerRuntimeAction::Previous => self.with_picker_state(
                env,
                to_picker_state(self).move_previous(&picker_items(env), &env.excluded_ids),
                false,
            ),
            PickerRuntimeAction::Home => self.with_picker_state(
                env,
                to_picker_state(self).move_home(&picker_items(env), &env.excluded_ids),
                false,
            ),
            PickerRuntimeAction::End => self.with_picker_state(
                env,
                to_picker_state(self).move_end(&picker_items(env), &env.excluded_ids),
                false,
            ),
            PickerRuntimeAction::Pick => {
                if self.active_item_key.as_deref() == Some("__root__") && env.allow_root_pick {
                    return transition(
                        self.clone(),
                        [PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Root)],
                    );
                }
                let items = picker_items(env);
                if let Some(document_id) = to_picker_state(self).pick(&items, &env.excluded_ids) {
                    transition(
                        self.clone(),
                        [PickerRuntimeOutput::Pick(
                            PickerRuntimePickTarget::Document { document_id },
                        )],
                    )
                } else {
                    transition(self.clone(), [])
                }
            }
        }
    }

    fn with_picker_state(
        &self,
        _env: &PickerRuntimeEnvironment,
        picker_state: PickerState,
        focus_dom: bool,
    ) -> PickerRuntimeTransition {
        transition(
            PickerRuntimeState {
                active_item_key: picker_state.active_item_key,
            },
            [PickerRuntimeOutput::DomPatch { focus_dom }],
        )
    }
}

fn picker_items(env: &PickerRuntimeEnvironment) -> Vec<PickerItem> {
    env.items
        .iter()
        .filter(|item| item.item_key != "__root__" || env.allow_root_pick)
        .map(|item| PickerItem {
            item_key: item.item_key.clone(),
            document_id: item.document_id.clone(),
            pickable: item.pickable,
        })
        .collect()
}

fn to_picker_state(state: &PickerRuntimeState) -> PickerState {
    PickerState {
        active_item_key: state.active_item_key.clone(),
    }
}

fn is_pickable_item_key(env: &PickerRuntimeEnvironment, item_key: &str) -> bool {
    picker_items(env).iter().any(|item| {
        item.pickable
            && item.item_key == item_key
            && item
                .document_id
                .as_ref()
                .is_none_or(|document_id| !env.excluded_ids.contains(document_id))
    })
}

fn transition<const N: usize>(
    state: PickerRuntimeState,
    outputs: [PickerRuntimeOutput; N],
) -> PickerRuntimeTransition {
    PickerRuntimeTransition {
        state,
        outputs: outputs.into_iter().collect(),
    }
}

#[cfg(test)]
mod tests {
    use super::{
        PickerRuntimeAction, PickerRuntimeEnvironment, PickerRuntimeItem, PickerRuntimeOutput,
        PickerRuntimePickTarget, PickerRuntimeState,
    };
    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) -> PickerRuntimeItem {
        PickerRuntimeItem {
            item_key: item_key.into(),
            document_id: document_id.map(ToOwned::to_owned),
            pickable,
        }
    }

    fn env() -> PickerRuntimeEnvironment {
        PickerRuntimeEnvironment {
            items: vec![
                item("__root__", None, true),
                item("doc:a", Some("doc:a"), true),
                item("doc:hidden", Some("doc:hidden"), true),
                item("doc:c", Some("doc:c"), true),
            ],
            excluded_ids: excluded(&["doc:hidden"]),
            allow_root_pick: true,
        }
    }

    #[test]
    fn picker_runtime_reducer_covers_navigation_pick_and_search_focus_boundary() {
        let state = PickerRuntimeState {
            active_item_key: Some("doc:hidden".into()),
        };

        let transition = state.reduce(&env(), PickerRuntimeAction::Normalize);
        assert_eq!(
            transition.state.active_item_key.as_deref(),
            Some("__root__")
        );
        assert!(transition
            .outputs
            .contains(&PickerRuntimeOutput::DomPatch { focus_dom: false }));

        let transition = transition.state.reduce(&env(), PickerRuntimeAction::Next);
        assert_eq!(transition.state.active_item_key.as_deref(), Some("doc:a"));
        assert!(transition
            .outputs
            .contains(&PickerRuntimeOutput::DomPatch { focus_dom: false }));

        let transition = transition.state.reduce(&env(), PickerRuntimeAction::End);
        assert_eq!(transition.state.active_item_key.as_deref(), Some("doc:c"));

        let transition = transition.state.reduce(
            &env(),
            PickerRuntimeAction::Focus {
                item_key: "__root__".into(),
                focus_dom: true,
            },
        );
        assert_eq!(
            transition.state.active_item_key.as_deref(),
            Some("__root__")
        );
        assert!(transition
            .outputs
            .contains(&PickerRuntimeOutput::DomPatch { focus_dom: true }));

        let transition = transition.state.reduce(&env(), PickerRuntimeAction::Pick);
        assert!(transition
            .outputs
            .contains(&PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Root,)));

        let transition = transition.state.reduce(
            &env(),
            PickerRuntimeAction::Focus {
                item_key: "doc:c".into(),
                focus_dom: true,
            },
        );
        let transition = transition.state.reduce(&env(), PickerRuntimeAction::Pick);
        assert!(transition.outputs.contains(&PickerRuntimeOutput::Pick(
            PickerRuntimePickTarget::Document {
                document_id: "doc:c".into(),
            },
        )));
    }
}
