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

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageTreeRuntimeEnvironment {
    pub visible_node_ids: Vec<String>,
    pub expandable_node_ids: BTreeSet<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub rows: Vec<PageTreeRuntimeRow>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageTreeRuntimeRow {
    pub node_id: String,
    pub parent_node_id: Option<String>,
    pub position: i64,
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageTreeRuntimeState {
    pub focused_id: Option<String>,
    pub expanded_ids: BTreeSet<String>,
    pub drop_feedback: Option<PageTreeDropFeedback>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageTreeRuntimeTransition {
    pub state: PageTreeRuntimeState,
    pub outputs: BTreeSet<PageTreeRuntimeOutput>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
    tag = "kind",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub enum PageTreeRuntimeAction {
    Normalize,
    Focus {
        node_id: String,
    },
    MoveNext,
    MovePrevious,
    MoveHome,
    MoveEnd,
    Expand {
        node_id: String,
    },
    Collapse {
        node_id: String,
    },
    Toggle {
        node_id: String,
    },
    OpenFocused,
    ContextMenuFocused,
    DispatchCreate {
        parent_node_id: Option<String>,
    },
    DispatchRename {
        node_id: String,
        title: String,
    },
    UpdateDropFeedback {
        feedback: Option<PageTreeDropFeedback>,
    },
    UpdateDropFeedbackForTarget {
        source_node_id: String,
        target_node_id: String,
        position: PageTreeDropPosition,
    },
    DispatchMove {
        source_node_id: String,
        target_parent_id: Option<String>,
        position: PageTreeDropPosition,
    },
    DispatchMoveToTarget {
        source_node_id: String,
        target_node_id: String,
        position: PageTreeDropPosition,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PageTreeRuntimeOutput {
    DomPatch,
    Intent(PageTreeIntentEvent),
    CommandDispatch(PageTreeCommandDispatchEvent),
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PageTreeIntentEvent {
    Open { node_id: String },
    ContextMenu { node_id: String },
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PageTreeCommandDispatchEvent {
    CreateNode {
        command_name: &'static str,
        parent_node_id: Option<String>,
    },
    RenameNode {
        command_name: &'static str,
        node_id: String,
        title: String,
    },
    MoveSubtree {
        command_name: &'static str,
        source_node_id: String,
        target_node_id: Option<String>,
        target_parent_id: Option<String>,
        position: PageTreeDropPosition,
        sort_order: Option<i64>,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageTreeDropFeedback {
    pub source_node_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_node_id: Option<String>,
    pub target_parent_id: Option<String>,
    pub position: PageTreeDropPosition,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PageTreeDropPosition {
    Before,
    Inside,
    After,
}

impl PageTreeRuntimeState {
    pub fn reduce(
        &self,
        env: &PageTreeRuntimeEnvironment,
        action: PageTreeRuntimeAction,
    ) -> PageTreeRuntimeTransition {
        match action {
            PageTreeRuntimeAction::Normalize => self.with_focus(
                TreeShellFocusState {
                    focused_id: self.focused_id.clone(),
                }
                .normalize(&env.visible_node_ids)
                .focused_id,
            ),
            PageTreeRuntimeAction::Focus { node_id } => {
                let focused_id = env
                    .visible_node_ids
                    .iter()
                    .any(|visible_id| visible_id == &node_id)
                    .then_some(node_id);
                self.with_focus(focused_id)
            }
            PageTreeRuntimeAction::MoveNext => self.with_focus(
                TreeShellFocusState {
                    focused_id: self.focused_id.clone(),
                }
                .move_next(&env.visible_node_ids)
                .focused_id,
            ),
            PageTreeRuntimeAction::MovePrevious => self.with_focus(
                TreeShellFocusState {
                    focused_id: self.focused_id.clone(),
                }
                .move_previous(&env.visible_node_ids)
                .focused_id,
            ),
            PageTreeRuntimeAction::MoveHome => self.with_focus(
                TreeShellFocusState {
                    focused_id: self.focused_id.clone(),
                }
                .move_home(&env.visible_node_ids)
                .focused_id,
            ),
            PageTreeRuntimeAction::MoveEnd => self.with_focus(
                TreeShellFocusState {
                    focused_id: self.focused_id.clone(),
                }
                .move_end(&env.visible_node_ids)
                .focused_id,
            ),
            PageTreeRuntimeAction::Expand { node_id } => self.with_expansion(env, node_id, true),
            PageTreeRuntimeAction::Collapse { node_id } => self.with_expansion(env, node_id, false),
            PageTreeRuntimeAction::Toggle { node_id } => {
                let expanded = !self.expanded_ids.contains(&node_id);
                self.with_expansion(env, node_id, expanded)
            }
            PageTreeRuntimeAction::OpenFocused => {
                self.with_focused_intent(PageTreeIntentEvent::Open {
                    node_id: self.focused_id.clone().unwrap_or_default(),
                })
            }
            PageTreeRuntimeAction::ContextMenuFocused => {
                self.with_focused_intent(PageTreeIntentEvent::ContextMenu {
                    node_id: self.focused_id.clone().unwrap_or_default(),
                })
            }
            PageTreeRuntimeAction::DispatchCreate { parent_node_id } => transition(
                self.clone(),
                [PageTreeRuntimeOutput::CommandDispatch(
                    PageTreeCommandDispatchEvent::CreateNode {
                        command_name: "tree.node.create",
                        parent_node_id,
                    },
                )],
            ),
            PageTreeRuntimeAction::DispatchRename { node_id, title } => {
                let title = title.trim().to_string();
                if node_id.trim().is_empty() || title.is_empty() {
                    return transition(self.clone(), []);
                }
                transition(
                    self.clone(),
                    [PageTreeRuntimeOutput::CommandDispatch(
                        PageTreeCommandDispatchEvent::RenameNode {
                            command_name: "tree.node.rename",
                            node_id,
                            title,
                        },
                    )],
                )
            }
            PageTreeRuntimeAction::UpdateDropFeedback { feedback } => {
                let mut state = self.clone();
                state.drop_feedback = feedback;
                transition(state, [PageTreeRuntimeOutput::DomPatch])
            }
            PageTreeRuntimeAction::UpdateDropFeedbackForTarget {
                source_node_id,
                target_node_id,
                position,
            } => {
                let mut state = self.clone();
                state.drop_feedback =
                    resolve_drop_feedback(env, &source_node_id, &target_node_id, position);
                transition(state, [PageTreeRuntimeOutput::DomPatch])
            }
            PageTreeRuntimeAction::DispatchMove {
                source_node_id,
                target_parent_id,
                position,
            } => transition(
                self.clone(),
                [PageTreeRuntimeOutput::CommandDispatch(
                    PageTreeCommandDispatchEvent::MoveSubtree {
                        command_name: "tree.subtree.move",
                        source_node_id,
                        target_node_id: None,
                        target_parent_id,
                        position,
                        sort_order: None,
                    },
                )],
            ),
            PageTreeRuntimeAction::DispatchMoveToTarget {
                source_node_id,
                target_node_id,
                position,
            } => {
                let Some(resolved_drop) =
                    resolve_drop_target(env, &source_node_id, &target_node_id, position)
                else {
                    return transition(self.clone(), []);
                };
                transition(
                    self.clone(),
                    [PageTreeRuntimeOutput::CommandDispatch(
                        PageTreeCommandDispatchEvent::MoveSubtree {
                            command_name: "tree.subtree.move",
                            source_node_id,
                            target_node_id: Some(target_node_id),
                            target_parent_id: resolved_drop.target_parent_id,
                            position,
                            sort_order: Some(resolved_drop.sort_order),
                        },
                    )],
                )
            }
        }
    }

    fn with_focus(&self, focused_id: Option<String>) -> PageTreeRuntimeTransition {
        let mut state = self.clone();
        state.focused_id = focused_id;
        transition(state, [PageTreeRuntimeOutput::DomPatch])
    }

    fn with_expansion(
        &self,
        env: &PageTreeRuntimeEnvironment,
        node_id: String,
        expanded: bool,
    ) -> PageTreeRuntimeTransition {
        let mut state = self.clone();
        if env.expandable_node_ids.contains(&node_id) {
            if expanded {
                state.expanded_ids.insert(node_id);
            } else {
                state.expanded_ids.remove(&node_id);
            }
        }
        transition(state, [PageTreeRuntimeOutput::DomPatch])
    }

    fn with_focused_intent(&self, intent: PageTreeIntentEvent) -> PageTreeRuntimeTransition {
        let has_focus = !match &intent {
            PageTreeIntentEvent::Open { node_id } => node_id,
            PageTreeIntentEvent::ContextMenu { node_id } => node_id,
        }
        .is_empty();
        if has_focus {
            transition(self.clone(), [PageTreeRuntimeOutput::Intent(intent)])
        } else {
            transition(self.clone(), [])
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ResolvedPageDropTarget {
    target_parent_id: Option<String>,
    sort_order: i64,
}

fn resolve_drop_feedback(
    env: &PageTreeRuntimeEnvironment,
    source_node_id: &str,
    target_node_id: &str,
    position: PageTreeDropPosition,
) -> Option<PageTreeDropFeedback> {
    let resolved = resolve_drop_target(env, source_node_id, target_node_id, position)?;
    Some(PageTreeDropFeedback {
        source_node_id: source_node_id.to_string(),
        target_node_id: Some(target_node_id.to_string()),
        target_parent_id: resolved.target_parent_id,
        position,
    })
}

fn resolve_drop_target(
    env: &PageTreeRuntimeEnvironment,
    source_node_id: &str,
    target_node_id: &str,
    position: PageTreeDropPosition,
) -> Option<ResolvedPageDropTarget> {
    let source_node_id = source_node_id.trim();
    let target_node_id = target_node_id.trim();
    if source_node_id.is_empty() || target_node_id.is_empty() || source_node_id == target_node_id {
        return None;
    }
    let source_row = env.rows.iter().find(|row| row.node_id == source_node_id)?;
    let target_row = env.rows.iter().find(|row| row.node_id == target_node_id)?;
    if source_row.parent_node_id != target_row.parent_node_id {
        return None;
    }
    let mut siblings = env
        .rows
        .iter()
        .filter(|row| row.parent_node_id == target_row.parent_node_id)
        .collect::<Vec<_>>();
    siblings.sort_by(|left, right| {
        left.position
            .cmp(&right.position)
            .then_with(|| left.node_id.cmp(&right.node_id))
    });
    let target_index = siblings
        .iter()
        .position(|row| row.node_id == target_row.node_id)? as i64;
    let sort_order = match position {
        PageTreeDropPosition::Before | PageTreeDropPosition::Inside => target_index,
        PageTreeDropPosition::After => target_index + 1,
    };
    Some(ResolvedPageDropTarget {
        target_parent_id: target_row.parent_node_id.clone(),
        sort_order,
    })
}

fn transition<const N: usize>(
    state: PageTreeRuntimeState,
    outputs: [PageTreeRuntimeOutput; N],
) -> PageTreeRuntimeTransition {
    PageTreeRuntimeTransition {
        state,
        outputs: outputs.into_iter().collect(),
    }
}

#[cfg(test)]
mod tests {
    use super::{
        PageTreeCommandDispatchEvent, PageTreeDropFeedback, PageTreeDropPosition,
        PageTreeIntentEvent, PageTreeRuntimeAction, PageTreeRuntimeEnvironment,
        PageTreeRuntimeOutput, PageTreeRuntimeRow, PageTreeRuntimeState,
    };

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

    #[test]
    fn page_tree_runtime_reducer_normalizes_focus_expansion_intents_and_drag_move() {
        let env = PageTreeRuntimeEnvironment {
            visible_node_ids: ids(&["doc:root", "doc:child", "doc:sibling"]),
            expandable_node_ids: ids(&["doc:root"]).into_iter().collect(),
            rows: vec![
                PageTreeRuntimeRow {
                    node_id: "doc:root".into(),
                    parent_node_id: None,
                    position: 0,
                },
                PageTreeRuntimeRow {
                    node_id: "doc:child".into(),
                    parent_node_id: Some("doc:root".into()),
                    position: 0,
                },
                PageTreeRuntimeRow {
                    node_id: "doc:sibling".into(),
                    parent_node_id: Some("doc:root".into()),
                    position: 1,
                },
            ],
        };
        let state = PageTreeRuntimeState {
            focused_id: Some("doc:missing".into()),
            ..PageTreeRuntimeState::default()
        };

        let transition = state.reduce(&env, PageTreeRuntimeAction::Normalize);
        assert_eq!(transition.state.focused_id.as_deref(), Some("doc:root"));
        assert!(transition
            .outputs
            .contains(&PageTreeRuntimeOutput::DomPatch));

        let transition = transition
            .state
            .reduce(&env, PageTreeRuntimeAction::MoveNext);
        assert_eq!(transition.state.focused_id.as_deref(), Some("doc:child"));

        let transition = transition.state.reduce(
            &env,
            PageTreeRuntimeAction::Expand {
                node_id: "doc:root".into(),
            },
        );
        assert!(transition.state.expanded_ids.contains("doc:root"));
        assert!(transition
            .outputs
            .contains(&PageTreeRuntimeOutput::DomPatch));

        let transition = transition
            .state
            .reduce(&env, PageTreeRuntimeAction::OpenFocused);
        assert!(transition.outputs.contains(&PageTreeRuntimeOutput::Intent(
            PageTreeIntentEvent::Open {
                node_id: "doc:child".into(),
            },
        )));

        let transition = transition
            .state
            .reduce(&env, PageTreeRuntimeAction::ContextMenuFocused);
        assert!(transition.outputs.contains(&PageTreeRuntimeOutput::Intent(
            PageTreeIntentEvent::ContextMenu {
                node_id: "doc:child".into(),
            },
        )));

        let transition = transition.state.reduce(
            &env,
            PageTreeRuntimeAction::UpdateDropFeedback {
                feedback: Some(PageTreeDropFeedback {
                    source_node_id: "doc:child".into(),
                    target_node_id: Some("doc:sibling".into()),
                    target_parent_id: Some("doc:root".into()),
                    position: PageTreeDropPosition::Inside,
                }),
            },
        );
        assert_eq!(
            transition
                .state
                .drop_feedback
                .as_ref()
                .map(|feedback| feedback.position),
            Some(PageTreeDropPosition::Inside)
        );

        let transition = transition.state.reduce(
            &env,
            PageTreeRuntimeAction::UpdateDropFeedbackForTarget {
                source_node_id: "doc:child".into(),
                target_node_id: "doc:sibling".into(),
                position: PageTreeDropPosition::Before,
            },
        );
        assert_eq!(
            transition
                .state
                .drop_feedback
                .as_ref()
                .and_then(|feedback| feedback.target_node_id.as_deref()),
            Some("doc:sibling")
        );

        let transition = transition.state.reduce(
            &env,
            PageTreeRuntimeAction::DispatchMove {
                source_node_id: "doc:child".into(),
                target_parent_id: Some("doc:root".into()),
                position: PageTreeDropPosition::After,
            },
        );
        assert!(transition
            .outputs
            .contains(&PageTreeRuntimeOutput::CommandDispatch(
                PageTreeCommandDispatchEvent::MoveSubtree {
                    command_name: "tree.subtree.move",
                    source_node_id: "doc:child".into(),
                    target_node_id: None,
                    target_parent_id: Some("doc:root".into()),
                    position: PageTreeDropPosition::After,
                    sort_order: None,
                },
            )));

        let transition = transition.state.reduce(
            &env,
            PageTreeRuntimeAction::DispatchMoveToTarget {
                source_node_id: "doc:child".into(),
                target_node_id: "doc:sibling".into(),
                position: PageTreeDropPosition::Before,
            },
        );
        assert!(transition
            .outputs
            .contains(&PageTreeRuntimeOutput::CommandDispatch(
                PageTreeCommandDispatchEvent::MoveSubtree {
                    command_name: "tree.subtree.move",
                    source_node_id: "doc:child".into(),
                    target_node_id: Some("doc:sibling".into()),
                    target_parent_id: Some("doc:root".into()),
                    position: PageTreeDropPosition::Before,
                    sort_order: Some(1),
                },
            )));

        let transition = transition.state.reduce(
            &env,
            PageTreeRuntimeAction::DispatchCreate {
                parent_node_id: Some("doc:root".into()),
            },
        );
        assert!(transition
            .outputs
            .contains(&PageTreeRuntimeOutput::CommandDispatch(
                PageTreeCommandDispatchEvent::CreateNode {
                    command_name: "tree.node.create",
                    parent_node_id: Some("doc:root".into()),
                },
            )));

        let transition = transition.state.reduce(
            &env,
            PageTreeRuntimeAction::DispatchRename {
                node_id: "doc:child".into(),
                title: "新标题".into(),
            },
        );
        assert!(transition
            .outputs
            .contains(&PageTreeRuntimeOutput::CommandDispatch(
                PageTreeCommandDispatchEvent::RenameNode {
                    command_name: "tree.node.rename",
                    node_id: "doc:child".into(),
                    title: "新标题".into(),
                },
            )));
    }
}
