//! Raw-node command application — `InsertNode` / `UpdateNode` /
//! `DeleteNode` / `MoveNode` / `CopyNode` / `ReplaceNode` /
//! `BatchInsert`.
//!
//! shell-core's `mcp_apply.rs` built its flat `Node` struct directly;
//! `op-editor-core` operates on the canonical `jian_ops_schema::PenNode`
//! tree, so this module ports the build / find / detach / replace
//! helpers onto `PenNode`. Carved off `command_apply.rs` to keep both
//! files under the 800-line cap.
//!
//! Every helper preserves the pre-validate-then-mutate discipline: a
//! caller validates kind / geometry / hex / id space BEFORE any tree
//! write, so a bad arg never leaves the document half-mutated.

use crate::command::BatchInsertItem;
use crate::fills::set_primary_fill_hex;
use crate::node_id::NodeId;
use crate::pen_node_ext::PenNodeExt;
use crate::state::EditorState;
use crate::walkers;
use jian_ops_schema::node::{
    ContainerProps, EllipseNode, FrameNode, GroupNode, LineNode, PathNode, PenNode, PenNodeBase,
    PolygonNode, RectangleNode, TextContent, TextNode,
};
use jian_ops_schema::sizing::SizingBehavior;
use std::collections::HashSet;

/// Resolve an editor `kind` arg into a canonical-schema leaf node.
/// Accepts the same lowercase strings the read-side tools emit
/// (`frame` / `group` / `rect` / `ellipse` / `polygon` / `line` /
/// `text` / `path`). `None` for an unknown kind.
///
/// `width` / `height` write the variant's literal `SizingBehavior`;
/// `(x, y)` write `base`. Container kinds (`frame` / `group`) start
/// with an empty `children` so a follow-up `MoveNode` can reparent
/// into them.
pub fn build_leaf_node(
    kind: &str,
    id: &str,
    name: &str,
    x: i32,
    y: i32,
    width: i32,
    height: i32,
) -> Option<PenNode> {
    let base = PenNodeBase {
        id: id.to_string(),
        name: Some(name.to_string()),
        x: Some(x as f64),
        y: Some(y as f64),
        ..Default::default()
    };
    let w = SizingBehavior::Number(width.max(0) as f64);
    let h = SizingBehavior::Number(height.max(0) as f64);
    let node = match kind {
        "frame" => PenNode::Frame(FrameNode {
            base,
            container: ContainerProps {
                width: Some(w),
                height: Some(h),
                ..Default::default()
            },
            children: Some(Vec::new()),
            reusable: None,
            slot: None,
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        "group" => PenNode::Group(GroupNode {
            base,
            container: ContainerProps {
                width: Some(w),
                height: Some(h),
                ..Default::default()
            },
            children: Some(Vec::new()),
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        "rect" => PenNode::Rectangle(RectangleNode {
            base,
            container: ContainerProps {
                width: Some(w),
                height: Some(h),
                ..Default::default()
            },
            children: None,
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        "ellipse" => PenNode::Ellipse(EllipseNode {
            base,
            width: Some(w),
            height: Some(h),
            corner_radius: None,
            inner_radius: None,
            start_angle: None,
            sweep_angle: None,
            fill: None,
            stroke: None,
            effects: None,
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        "polygon" => PenNode::Polygon(PolygonNode {
            base,
            polygon_count: 3,
            width: Some(w),
            height: Some(h),
            corner_radius: None,
            fill: None,
            stroke: None,
            effects: None,
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        "line" => PenNode::Line(LineNode {
            base,
            x2: Some((x + width) as f64),
            y2: Some((y + height) as f64),
            stroke: None,
            effects: None,
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        "text" => PenNode::Text(TextNode {
            base,
            width: Some(w),
            height: Some(h),
            content: TextContent::Plain(name.to_string()),
            font_family: None,
            font_size: None,
            font_weight: None,
            font_style: None,
            letter_spacing: None,
            line_height: None,
            text_align: None,
            text_align_vertical: None,
            text_growth: None,
            underline: None,
            strikethrough: None,
            fill: None,
            effects: None,
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        "path" => PenNode::Path(PathNode {
            base,
            icon_id: None,
            d: None,
            anchors: Some(Vec::new()),
            closed: None,
            width: Some(w),
            height: Some(h),
            fill: None,
            stroke: None,
            effects: None,
            state: None,
            bindings: None,
            events: None,
            lifecycle: None,
            semantics: None,
            gestures: None,
            route: None,
        }),
        _ => return None,
    };
    Some(node)
}

/// True when `kind` resolves to a buildable leaf node. Used by the
/// `BatchInsert` pre-validation pass.
pub fn kind_is_valid(kind: &str) -> bool {
    matches!(
        kind,
        "frame" | "group" | "rect" | "ellipse" | "polygon" | "line" | "text" | "path"
    )
}

/// Replace the node with `target` id with `replacement` at its current
/// slot, preserving sibling order. True on the first match.
pub fn replace_node_in_children(
    children: &mut [PenNode],
    target: &NodeId,
    replacement: &mut Option<PenNode>,
) -> bool {
    if let Some(idx) = children.iter().position(|n| n.id_str() == target.as_str()) {
        if let Some(r) = replacement.take() {
            children[idx] = r;
            return true;
        }
    }
    for child in children.iter_mut() {
        if let Some(grand) = child.children_mut() {
            if replace_node_in_children(grand, target, replacement) {
                return true;
            }
        }
    }
    false
}

impl EditorState {
    /// Compute the numeric seed for the next editor-minted `n{N}` id —
    /// `max_node_id() + 1`. `None` on `u64` exhaustion.
    pub(crate) fn next_node_id_seed(&self) -> Option<u64> {
        self.max_node_id().checked_add(1).map(|n| n.max(1))
    }

    /// Allocate one fresh `n{N}` id, skipping any candidate that
    /// collides with a live id. `None` on counter exhaustion.
    pub(crate) fn next_node_id(&self) -> Option<NodeId> {
        let mut seed = self.next_node_id_seed()?;
        let mut live = self.collect_node_ids();
        walkers::alloc_n_id(&mut seed, &mut live)
    }

    /// `InsertNode` — build + append a fresh leaf on the active page.
    // Args mirror the `InsertNode` command fields one-for-one; bundling
    // them into a struct would just shadow the DTO with no real gain.
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn cmd_insert_node(
        &mut self,
        kind: &str,
        name: &str,
        x: i32,
        y: i32,
        width: i32,
        height: i32,
        fill_hex: &Option<String>,
    ) -> bool {
        if !kind_is_valid(kind) || width < 0 || height < 0 {
            return false;
        }
        // Pre-validate hex BEFORE minting an id / mutating the tree.
        if let Some(hex) = fill_hex {
            if crate::color_picker::parse_hex_rgb(hex).is_none() {
                return false;
            }
        }
        let Some(new_id) = self.next_node_id() else {
            return false;
        };
        let Some(mut node) = build_leaf_node(kind, new_id.as_str(), name, x, y, width, height)
        else {
            return false;
        };
        if let Some(hex) = fill_hex {
            set_primary_fill_hex(&mut node, hex);
        }
        self.active_children_mut().push(node);
        true
    }

    /// `UpdateNode` — patch optional fields on an existing node.
    // Args mirror the `UpdateNode` command fields one-for-one.
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn cmd_update_node(
        &mut self,
        node_id: &NodeId,
        x: Option<i32>,
        y: Option<i32>,
        width: Option<i32>,
        height: Option<i32>,
        name: &Option<String>,
        fill_hex: &Option<String>,
    ) -> bool {
        if !node_id.is_real() {
            return false;
        }
        // Pre-validate EVERY field before the mutable borrow + writes.
        if let Some(w) = width {
            if w < 0 {
                return false;
            }
        }
        if let Some(h) = height {
            if h < 0 {
                return false;
            }
        }
        if let Some(hex) = fill_hex {
            if crate::color_picker::parse_hex_rgb(hex).is_none() {
                return false;
            }
        }
        let Some(node) = walkers::find_node_mut(self.active_children_mut(), node_id) else {
            return false;
        };
        // All validation passed — every field applies atomically.
        if let Some(nx) = x {
            node.base_mut().x = Some(nx as f64);
        }
        if let Some(ny) = y {
            node.base_mut().y = Some(ny as f64);
        }
        if let Some(nw) = width {
            node.set_width_px(nw as f64);
        }
        if let Some(nh) = height {
            node.set_height_px(nh as f64);
        }
        if let Some(new_name) = name {
            node.base_mut().name = Some(new_name.clone());
        }
        if let Some(hex) = fill_hex {
            set_primary_fill_hex(node, hex);
        }
        true
    }

    /// `DeleteNode` — remove a node + descendants from the active page.
    pub(crate) fn cmd_delete_node(&mut self, node_id: &NodeId) -> bool {
        if !node_id.is_real() {
            return false;
        }
        walkers::remove_from_children(self.active_children_mut(), node_id)
    }

    /// `MoveNode` — reparent a node. A `NONE` target reparents to the
    /// active page root; a real target must resolve + must not create
    /// a cycle (target is a descendant of the moved node).
    pub(crate) fn cmd_move_node(&mut self, node_id: &NodeId, target_parent: &NodeId) -> bool {
        if !node_id.is_real() || target_parent == node_id {
            return false;
        }
        // Pre-validate EVERYTHING before detaching: a bad target would
        // detach → reattach-fail → silently drop the source.
        {
            let children = self.active_children();
            let Some(src) = walkers::find_node(children, node_id) else {
                return false;
            };
            if target_parent.is_real() {
                if walkers::descendant_contains(src, target_parent) {
                    return false;
                }
                if walkers::find_node(children, target_parent).is_none() {
                    return false;
                }
            }
        }
        let children = self.active_children_mut();
        let Some(detached) = walkers::extract_node(children, node_id) else {
            return false;
        };
        if target_parent.is_real() {
            walkers::append_into(children, target_parent, detached).is_ok()
        } else {
            children.push(detached);
            true
        }
    }

    /// `CopyNode` — deep-clone a node + subtree under a new parent
    /// (`NONE` = active page root). Fresh ids minted past the id space.
    pub(crate) fn cmd_copy_node(&mut self, node_id: &NodeId, target_parent: &NodeId) -> bool {
        if !node_id.is_real() {
            return false;
        }
        // Validate source + target up front.
        {
            let children = self.active_children();
            if walkers::find_node(children, node_id).is_none() {
                return false;
            }
            if target_parent.is_real() && walkers::find_node(children, target_parent).is_none() {
                return false;
            }
        }
        let Some(mut next_id) = self.next_node_id_seed() else {
            return false;
        };
        let mut taken = self.collect_node_ids();
        // Clone the owned subtree before re-borrowing the tree mutably.
        let clone = {
            let children = self.active_children();
            let src = walkers::find_node(children, node_id).expect("validated");
            walkers::deep_clone_with_new_ids(src, &mut next_id, &mut taken)
        };
        let children = self.active_children_mut();
        if target_parent.is_real() {
            walkers::append_into(children, target_parent, clone).is_ok()
        } else {
            children.push(clone);
            true
        }
    }

    /// `ReplaceNode` — swap an existing node for a freshly-built leaf at
    /// the same slot. The destructive-swap guard: replacing a node WITH
    /// children requires `drop_children == true`, else the swap is
    /// refused so a container can't silently lose its subtree.
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn cmd_replace_node(
        &mut self,
        node_id: &NodeId,
        kind: &str,
        name: &str,
        x: i32,
        y: i32,
        width: i32,
        height: i32,
        fill_hex: &Option<String>,
        drop_children: bool,
    ) -> bool {
        if !node_id.is_real() || !kind_is_valid(kind) || width < 0 || height < 0 {
            return false;
        }
        if let Some(hex) = fill_hex {
            if crate::color_picker::parse_hex_rgb(hex).is_none() {
                return false;
            }
        }
        // Resolve target + check the destructive-swap guard BEFORE
        // minting an id. A target WITH children needs explicit consent.
        {
            let children = self.active_children();
            let Some(target) = walkers::find_node(children, node_id) else {
                return false;
            };
            let has_children = target.children().map(|c| !c.is_empty()).unwrap_or(false);
            if has_children && !drop_children {
                return false;
            }
        }
        let Some(new_id) = self.next_node_id() else {
            return false;
        };
        let Some(mut replacement) =
            build_leaf_node(kind, new_id.as_str(), name, x, y, width, height)
        else {
            return false;
        };
        if let Some(hex) = fill_hex {
            set_primary_fill_hex(&mut replacement, hex);
        }
        let mut slot = Some(replacement);
        replace_node_in_children(self.active_children_mut(), node_id, &mut slot)
    }

    /// `BatchInsert` — insert N leaf nodes on the active page in one
    /// atomic shot. EVERY descriptor is validated before any mutation;
    /// a single bad entry rejects the whole batch.
    pub(crate) fn cmd_batch_insert(&mut self, items: &[BatchInsertItem]) -> bool {
        if items.is_empty() {
            return false;
        }
        // Pre-validate kinds + geometry + fill hex up front.
        for item in items {
            if !kind_is_valid(&item.kind) || item.width < 0 || item.height < 0 {
                return false;
            }
            if let Some(hex) = &item.fill_hex {
                if crate::color_picker::parse_hex_rgb(hex).is_none() {
                    return false;
                }
            }
        }
        // Allocate every fresh id up front; bail on id-space exhaustion.
        let Some(mut next_id) = self.next_node_id_seed() else {
            return false;
        };
        let mut live: HashSet<NodeId> = self.collect_node_ids();
        let mut ids: Vec<NodeId> = Vec::with_capacity(items.len());
        for _ in 0..items.len() {
            match walkers::alloc_n_id(&mut next_id, &mut live) {
                Some(id) => ids.push(id),
                None => return false,
            }
        }
        // All validation + allocation passed — now mutate.
        let children = self.active_children_mut();
        for (item, id) in items.iter().zip(ids) {
            // `kind` already validated, so `build_leaf_node` is Some.
            let mut node = build_leaf_node(
                &item.kind,
                id.as_str(),
                &item.name,
                item.x,
                item.y,
                item.width,
                item.height,
            )
            .expect("kind validated");
            if let Some(hex) = &item.fill_hex {
                set_primary_fill_hex(&mut node, hex);
            }
            children.push(node);
        }
        true
    }

    /// Insert one or more nested `PenNode` subtrees. `parent_id` of
    /// `NONE` appends to the active page root; otherwise the parent
    /// must exist and be a container variant. Every incoming node id
    /// (recursively) is remapped to a fresh editor id so an
    /// externally-authored subtree can't collide with live doc ids.
    pub(crate) fn cmd_insert_subtree(&mut self, nodes: Vec<PenNode>, parent_id: &NodeId) -> bool {
        if nodes.is_empty() {
            return false;
        }
        // Validate the parent up front (when not the page root).
        if parent_id.is_real() {
            match walkers::find_node(self.active_children(), parent_id) {
                Some(p) if p.is_container() => {}
                _ => return false, // missing or non-container
            }
        }
        // Allocate fresh ids for the whole incoming forest.
        let Some(mut next_id) = self.next_node_id_seed() else {
            return false;
        };
        let mut taken: HashSet<NodeId> = self.collect_node_ids();
        let mut nodes = nodes;
        if !remap_subtree_ids(&mut nodes, &mut next_id, &mut taken) {
            return false;
        }
        // All validation + allocation passed — now mutate.
        if parent_id.is_real() {
            let root = self.active_children_mut();
            let Some(parent) = walkers::find_node_mut(root, parent_id) else {
                return false;
            };
            let Some(slot) = parent.children_mut() else {
                return false;
            };
            slot.extend(nodes);
        } else {
            self.active_children_mut().extend(nodes);
        }
        true
    }
}

/// Reassign every node id in `nodes` (recursively, including
/// `children`) to a fresh unique id. `next_id` + `taken` are the same
/// allocator pair [`walkers::alloc_n_id`] uses; `taken` must be seeded
/// with the document's live ids. Returns `false` on id-space
/// exhaustion.
pub(crate) fn remap_subtree_ids(
    nodes: &mut [PenNode],
    next_id: &mut u64,
    taken: &mut HashSet<NodeId>,
) -> bool {
    for node in nodes.iter_mut() {
        let Some(fresh) = walkers::alloc_n_id(next_id, taken) else {
            return false;
        };
        node.base_mut().id = fresh.as_str().to_string();
        if let Some(children) = node.children_mut() {
            if !remap_subtree_ids(children, next_id, taken) {
                return false;
            }
        }
    }
    true
}
