//! Patch application utilities
//!
//! Utilities for applying patches to virtual DOM trees.
//! In the LiveView system, patches are serialized and sent to the client.

use crate::{Patch, VNode};

/// Apply a list of patches to a virtual DOM tree (for testing purposes).
///
/// The diff engine emits patches with indices referencing the **new** (target)
/// tree. Child mutations (Insert/Remove/Move) are grouped by parent and applied
/// in the order: removes (descending index), inserts (ascending index), then
/// moves (resolved by djust_id). Attribute/text patches are applied last, using
/// djust_id-based node lookup when available so that path indices invalidated by
/// earlier structural changes don't cause mis-targeting.
pub fn apply_patches(root: &mut VNode, patches: &[Patch]) {
    // Group child-mutation patches by parent djust_id, preserving order within
    // each group. Use BTreeMap keyed by first-seen order to process parents in
    // the order they appear in the patch list (shallowest first due to diff
    // engine's top-down traversal).
    struct ParentGroup<'a> {
        removes: Vec<(&'a Patch, usize)>, // (patch, index)
        inserts: Vec<(&'a Patch, usize)>, // (patch, index)
        moves: Vec<&'a Patch>,
    }

    let mut parent_order: Vec<String> = Vec::new();
    let mut parent_groups: std::collections::HashMap<String, ParentGroup<'_>> =
        std::collections::HashMap::new();
    let mut non_child_patches: Vec<&Patch> = Vec::new();

    for patch in patches {
        match patch {
            Patch::RemoveChild { d, index, .. } => {
                // Child-mutation patches should always have a parent djust_id
                // when generated by the diff engine on id-assigned trees.
                debug_assert!(
                    d.is_some(),
                    "RemoveChild patch missing parent djust_id (d=None)"
                );
                if let Some(ref pid) = d {
                    let group = parent_groups.entry(pid.clone()).or_insert_with(|| {
                        parent_order.push(pid.clone());
                        ParentGroup {
                            removes: Vec::new(),
                            inserts: Vec::new(),
                            moves: Vec::new(),
                        }
                    });
                    group.removes.push((patch, *index));
                }
            }
            Patch::InsertChild { d, index, .. } => {
                debug_assert!(
                    d.is_some(),
                    "InsertChild patch missing parent djust_id (d=None)"
                );
                if let Some(ref pid) = d {
                    let group = parent_groups.entry(pid.clone()).or_insert_with(|| {
                        parent_order.push(pid.clone());
                        ParentGroup {
                            removes: Vec::new(),
                            inserts: Vec::new(),
                            moves: Vec::new(),
                        }
                    });
                    group.inserts.push((patch, *index));
                }
            }
            Patch::MoveChild { d, .. } => {
                debug_assert!(
                    d.is_some(),
                    "MoveChild patch missing parent djust_id (d=None)"
                );
                if let Some(ref pid) = d {
                    let group = parent_groups.entry(pid.clone()).or_insert_with(|| {
                        parent_order.push(pid.clone());
                        ParentGroup {
                            removes: Vec::new(),
                            inserts: Vec::new(),
                            moves: Vec::new(),
                        }
                    });
                    group.moves.push(patch);
                }
            }
            _ => {
                non_child_patches.push(patch);
            }
        }
    }

    // Apply child mutations parent-by-parent in order.
    for pid in &parent_order {
        let group = parent_groups.get(pid).unwrap();

        // Removes: descending index order
        let mut removes: Vec<_> = group.removes.iter().collect();
        removes.sort_by(|a, b| b.1.cmp(&a.1));
        for (_, index) in &removes {
            if let Some(target) = find_by_djust_id_mut(root, pid) {
                if *index < target.children.len() {
                    target.children.remove(*index);
                }
            }
        }

        // Inserts: ascending index order
        let mut inserts: Vec<_> = group.inserts.iter().collect();
        inserts.sort_by(|a, b| a.1.cmp(&b.1));
        for (patch, _) in &inserts {
            if let Patch::InsertChild { index, node, .. } = patch {
                if let Some(target) = find_by_djust_id_mut(root, pid) {
                    let insert_at = (*index).min(target.children.len());
                    target.children.insert(insert_at, (*node).clone());
                }
            }
        }

        // Moves: by child_d (djust_id of the child to move)
        for patch in &group.moves {
            if let Patch::MoveChild {
                from, to, child_d, ..
            } = patch
            {
                if let Some(target) = find_by_djust_id_mut(root, pid) {
                    if let Some(ref cid) = child_d {
                        if let Some(current_pos) = target
                            .children
                            .iter()
                            .position(|c| c.djust_id.as_deref() == Some(cid.as_str()))
                        {
                            let node = target.children.remove(current_pos);
                            let insert_at = (*to).min(target.children.len());
                            target.children.insert(insert_at, node);
                        }
                    } else if *from < target.children.len() {
                        let node = target.children.remove(*from);
                        let insert_at = (*to).min(target.children.len());
                        target.children.insert(insert_at, node);
                    }
                }
            }
        }
    }

    // Apply non-child patches using djust_id resolution when available,
    // falling back to path-based traversal for text nodes (d=None).
    for patch in &non_child_patches {
        match patch {
            Patch::SetText { path, d, text } => {
                let target = resolve_node_mut(root, path, d.as_deref());
                if let Some(target) = target {
                    target.text = Some(text.clone());
                }
            }
            Patch::SetAttr {
                path,
                d,
                key,
                value,
            } => {
                let target = resolve_node_mut(root, path, d.as_deref());
                if let Some(target) = target {
                    target.attrs.insert(key.clone(), value.clone());
                }
            }
            Patch::RemoveAttr { path, d, key } => {
                let target = resolve_node_mut(root, path, d.as_deref());
                if let Some(target) = target {
                    target.attrs.remove(key);
                }
            }
            Patch::Replace { path, d, node } => {
                let target = resolve_node_mut(root, path, d.as_deref());
                if let Some(target) = target {
                    *target = node.clone();
                }
            }
            _ => {}
        }
    }
}

/// Resolve a node: try djust_id first, fall back to path traversal.
///
/// Note: This does two tree walks when using djust_id — an immutable check
/// followed by a mutable lookup. This is required because Rust's borrow checker
/// won't allow attempting a mutable borrow and falling back on failure within
/// the same scope. Acceptable for test-only code; production use should build
/// an id→node index upfront.
fn resolve_node_mut<'a>(
    root: &'a mut VNode,
    path: &[usize],
    djust_id: Option<&str>,
) -> Option<&'a mut VNode> {
    let use_id = djust_id
        .map(|id| find_by_djust_id(root, id).is_some())
        .unwrap_or(false);
    if use_id {
        find_by_djust_id_mut(root, djust_id.unwrap())
    } else {
        get_node_mut(root, path)
    }
}

/// Find a node by its djust_id (immutable).
fn find_by_djust_id<'a>(root: &'a VNode, id: &str) -> Option<&'a VNode> {
    if root.djust_id.as_deref() == Some(id) {
        return Some(root);
    }
    for child in &root.children {
        if let Some(found) = find_by_djust_id(child, id) {
            return Some(found);
        }
    }
    None
}

/// Find a node by its djust_id (mutable).
fn find_by_djust_id_mut<'a>(root: &'a mut VNode, id: &str) -> Option<&'a mut VNode> {
    if root.djust_id.as_deref() == Some(id) {
        return Some(root);
    }
    for child in &mut root.children {
        if let Some(found) = find_by_djust_id_mut(child, id) {
            return Some(found);
        }
    }
    None
}

/// Apply a single patch to a virtual DOM tree (for testing purposes)
///
/// Note: For correct `MoveChild` handling with multiple moves, prefer
/// `apply_patches()` which resolves children by `djust_id`. This function
/// uses index-based `MoveChild` which may produce incorrect results when
/// multiple moves shift indices.
pub fn apply_patch(root: &mut VNode, patch: &Patch) {
    match patch {
        Patch::Replace { path, node, .. } => {
            if let Some(target) = get_node_mut(root, path) {
                *target = node.clone();
            }
        }

        Patch::SetText { path, text, .. } => {
            if let Some(target) = get_node_mut(root, path) {
                target.text = Some(text.clone());
            }
        }

        Patch::SetAttr {
            path, key, value, ..
        } => {
            if let Some(target) = get_node_mut(root, path) {
                target.attrs.insert(key.clone(), value.clone());
            }
        }

        Patch::RemoveAttr { path, key, .. } => {
            if let Some(target) = get_node_mut(root, path) {
                target.attrs.remove(key);
            }
        }

        Patch::InsertChild {
            path, index, node, ..
        } => {
            if let Some(target) = get_node_mut(root, path) {
                if *index <= target.children.len() {
                    target.children.insert(*index, node.clone());
                }
            }
        }

        Patch::RemoveChild { path, index, .. } => {
            if let Some(target) = get_node_mut(root, path) {
                if *index < target.children.len() {
                    target.children.remove(*index);
                }
            }
        }

        Patch::MoveChild { path, from, to, .. } => {
            if let Some(target) = get_node_mut(root, path) {
                if *from < target.children.len() {
                    let node = target.children.remove(*from);
                    let insert_at = (*to).min(target.children.len());
                    target.children.insert(insert_at, node);
                }
            }
        }
    }
}

fn get_node_mut<'a>(root: &'a mut VNode, path: &[usize]) -> Option<&'a mut VNode> {
    let mut current = root;

    for &index in path {
        if index < current.children.len() {
            current = &mut current.children[index];
        } else {
            return None;
        }
    }

    Some(current)
}

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

    #[test]
    fn test_apply_set_text() {
        let mut root = VNode::text("old");
        let patch = Patch::SetText {
            path: vec![],
            d: None,
            text: "new".to_string(),
        };

        apply_patch(&mut root, &patch);
        assert_eq!(root.text, Some("new".to_string()));
    }

    #[test]
    fn test_apply_set_attr() {
        let mut root = VNode::element("div");
        let patch = Patch::SetAttr {
            path: vec![],
            d: Some("0".to_string()),
            key: "class".to_string(),
            value: "active".to_string(),
        };

        apply_patch(&mut root, &patch);
        assert_eq!(root.attrs.get("class"), Some(&"active".to_string()));
    }

    #[test]
    fn test_apply_insert_child() {
        let mut root = VNode::element("div");
        let patch = Patch::InsertChild {
            path: vec![],
            d: Some("0".to_string()),
            index: 0,
            node: VNode::text("child"),
        };

        apply_patch(&mut root, &patch);
        assert_eq!(root.children.len(), 1);
    }
}
