use cirru_parser::Cirru;
use colored::Colorize;
use std::fmt::Write as _;

use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, fragment_nesting_level, maybe_chunk_node};
use super::common::{
  ERR_CODE_INPUT_REQUIRED, cirru_to_json, emit_cli_output, format_path, format_path_bracketed, parse_input_to_cirru, parse_path,
  print_cli_warning_block, read_code_input, resolve_definition_lookup,
};
use super::tips::{TipPriority, Tips, command_guidance_enabled, tip_prefer_oneliner_json, tip_root_edit};
use crate::cli_args::{
  TreeAppendChildCommand, TreeBatchDeleteCommand, TreeCommand, TreeDeleteCommand, TreeInsertAfterCommand, TreeInsertBeforeCommand,
  TreeInsertChildCommand, TreeRaiseCommand, TreeReplaceCommand, TreeReplaceLeafCommand, TreeSearchReplaceCommand, TreeShowCommand,
  TreeStructuralCommand, TreeSubcommand, TreeSwapNextCommand, TreeSwapPrevCommand, TreeUnwrapCommand, TreeWrapCommand,
};

// Import shared functions from edit module
use super::edit::{
  apply_operation_at_path, check_ns_editable, load_snapshot, navigate_to_path, parse_target, process_node_with_references,
  save_snapshot,
};

// ═══════════════════════════════════════════════════════════════════════════════
// Helper functions
// ═══════════════════════════════════════════════════════════════════════════════

/// Main handler for code command
pub fn handle_tree_command(cmd: &TreeCommand, snapshot_file: &str) -> Result<(), String> {
  match &cmd.subcommand {
    TreeSubcommand::Show(opts) => handle_show(opts, snapshot_file, opts.json),
    TreeSubcommand::Replace(opts) => handle_replace(opts, snapshot_file),
    TreeSubcommand::ReplaceLeaf(opts) => handle_replace_leaf(opts, snapshot_file),
    TreeSubcommand::Delete(opts) => handle_delete(opts, snapshot_file),
    TreeSubcommand::InsertBefore(opts) => handle_insert_before(opts, snapshot_file),
    TreeSubcommand::InsertAfter(opts) => handle_insert_after(opts, snapshot_file),
    TreeSubcommand::InsertChild(opts) => handle_insert_child(opts, snapshot_file),
    TreeSubcommand::AppendChild(opts) => handle_append_child(opts, snapshot_file),
    TreeSubcommand::SwapNext(opts) => handle_swap_next(opts, snapshot_file),
    TreeSubcommand::SwapPrev(opts) => handle_swap_prev(opts, snapshot_file),
    TreeSubcommand::Unwrap(opts) => handle_unwrap(opts, snapshot_file),
    TreeSubcommand::Raise(opts) => handle_raise(opts, snapshot_file),
    TreeSubcommand::Wrap(opts) => handle_wrap(opts, snapshot_file),
    TreeSubcommand::SearchReplace(opts) => handle_search_replace(opts, snapshot_file),
    TreeSubcommand::Rewrite(opts) => handle_rewrite(opts, snapshot_file),
    TreeSubcommand::BatchDelete(opts) => handle_batch_delete(opts, snapshot_file),
  }
}

fn parse_with_references(with_strs: &[String], original_node: &Cirru) -> Result<std::collections::BTreeMap<String, Cirru>, String> {
  let mut references = std::collections::BTreeMap::new();

  for s in with_strs {
    let (name, path_str) = s
      .split_once('=')
      .ok_or_else(|| format!("Invalid --with format '{s}'. Expected 'name=path', e.g. --with self=. --with arg=1,0"))?;

    if name.trim().is_empty() {
      return Err(format!("Invalid --with format '{s}': name cannot be empty"));
    }

    let path = if path_str.trim() == "." {
      vec![]
    } else {
      parse_path(path_str.trim())?
    };

    let node = navigate_to_path(original_node, &path)?;
    references.insert(name.trim().to_string(), node.clone());
  }

  Ok(references)
}

/// Format a Cirru node for preview display
fn format_preview(node: &Cirru, max_lines: usize) -> String {
  let formatted = match node {
    Cirru::Leaf(s) => {
      // For leaf nodes, just show the value
      format!("  {:?}", s.as_ref())
    }
    Cirru::List(_) => {
      // For list nodes, use the Cirru formatter
      match cirru_parser::format(std::slice::from_ref(node), cirru_parser::CirruWriterOptions { use_inline: false }) {
        Ok(cirru_str) => {
          let lines: Vec<&str> = cirru_str.lines().collect();
          if lines.len() > max_lines {
            let mut result = String::new();
            for line in lines.iter().take(max_lines) {
              result.push_str("  ");
              result.push_str(line);
              result.push('\n');
            }
            result.push_str(&format!("  {}\n", format!("... ({} more lines)", lines.len() - max_lines).dimmed()));
            result
          } else {
            lines.iter().map(|line| format!("  {line}\n")).collect()
          }
        }
        Err(e) => format!("  {}\n", format!("(failed to format: {e})").red()),
      }
    }
  };
  formatted.trim_end().to_string()
}

/// Format a Cirru node for preview display with type annotation for short content
fn format_preview_with_type(node: &Cirru, max_lines: usize) -> String {
  let base_preview = format_preview(node, max_lines);

  // Add type annotation for short content (especially single-element expressions)
  let type_label = match node {
    Cirru::Leaf(_) => " (leaf)".dimmed().to_string(),
    Cirru::List(items) => {
      if items.len() == 1 {
        " (expr)".dimmed().to_string()
      } else {
        String::new()
      }
    }
  };

  if type_label.is_empty() {
    base_preview
  } else {
    format!("{base_preview}{type_label}")
  }
}

fn print_preview_block(label: &str, node: &Cirru, max_lines: usize, color: &str) {
  let label_text = match color {
    "yellow" => label.yellow().bold(),
    "green" => label.green().bold(),
    "cyan" => label.cyan().bold(),
    _ => label.bold(),
  };
  println!("{label_text}:");
  for line in format_preview_with_type(node, max_lines).lines() {
    println!("  {line}");
  }
}

/// Find the first leaf (preorder) and format for preview
fn first_leaf_preview(node: &Cirru) -> Option<String> {
  match node {
    Cirru::Leaf(s) => Some(format!("{:?}", s.as_ref())),
    Cirru::List(items) => items.iter().find_map(first_leaf_preview),
  }
}

/// Format a child node for list preview (leaf or list with first leaf)
fn format_child_preview(node: &Cirru) -> String {
  match node {
    Cirru::Leaf(s) => format!("{:?}", s.as_ref()),
    Cirru::List(items) => {
      if items.is_empty() {
        "(empty)".to_string()
      } else {
        let head = first_leaf_preview(node).unwrap_or_else(|| "<expr>".to_string());
        format!("({head} ...)")
      }
    }
  }
}

/// Show a side-by-side diff preview of the change
fn show_diff_preview(old_node: &Cirru, new_node: &Cirru, operation: &str) -> String {
  let mut output = String::new();

  output.push_str(&format!("\n{}: {}\n", "Preview".blue().bold(), operation));
  output.push('\n');

  // Show old and new side by side (simplified version)
  let old_preview = format_preview_with_type(old_node, 10);
  let new_preview = format_preview_with_type(new_node, 10);

  output.push_str(&format!("{}:\n", "Before".yellow().bold()));
  output.push_str(&old_preview);
  output.push_str("\n\n");
  output.push_str(&format!("{}:\n", "After".green().bold()));
  output.push_str(&new_preview);
  output.push('\n');

  output
}

fn render_chunked_display(display: &ChunkedDisplay, chunk_expand_depth: usize) -> (String, usize) {
  let mut out = String::new();
  let _ = writeln!(&mut out, "{}", "Chunked preview".green().bold());
  let _ = writeln!(
    &mut out,
    "{}",
    format!(
      "nodes: {}, branches: {}, leaves: {}, max depth: {}, fragments: {}",
      display.total.nodes,
      display.total.branches,
      display.total.leaves,
      display.total.max_depth,
      display.fragments.len()
    )
    .dimmed()
  );
  let _ = writeln!(&mut out);

  let visible_fragments: Vec<_> = display
    .fragments
    .iter()
    .filter(|fragment| fragment_nesting_level(fragment, &display.fragments) <= chunk_expand_depth)
    .collect();

  if visible_fragments.len() < display.fragments.len() {
    let _ = writeln!(
      &mut out,
      "{}",
      format!(
        "showing {}/{} fragments; nested chunks beyond level {} are hidden",
        visible_fragments.len(),
        display.fragments.len(),
        chunk_expand_depth
      )
      .dimmed()
    );
    let _ = writeln!(&mut out);
  }

  for fragment in &visible_fragments {
    let _ = writeln!(
      &mut out,
      "{} {}",
      fragment.id.cyan().bold(),
      format!("at {}", fragment.coord).dimmed()
    );
    let _ = writeln!(
      &mut out,
      "{}",
      format!("nodes: {}, max depth: {}", fragment.nodes, fragment.depth).dimmed()
    );
    for line in fragment.cirru.lines() {
      let _ = writeln!(&mut out, "  {line}");
    }
    let _ = writeln!(&mut out);
  }

  (out, visible_fragments.len())
}

// ============================================================================
// Command handlers
// ============================================================================

fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;
  let path = match &opts.path {
    Some(p) => parse_path(p)?,
    None => {
      eprintln!(
        "{}",
        "[Warn] No path (-p) specified; showing from root. Use -p '0' to start from a child node.".yellow()
      );
      vec![]
    }
  };

  let snapshot = load_snapshot(snapshot_file)?;

  let file_data = snapshot
    .files
    .get(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let lookup = resolve_definition_lookup(namespace, definition, file_data.defs.keys().map(|name| name.as_str()), true)?;
  let render_to_stderr = lookup.warning.is_some();
  if let Some(warning) = lookup.warning.as_deref() {
    print_cli_warning_block(warning);
  }
  let resolved_definition = lookup.resolved;
  let resolved_target = format!("{namespace}/{resolved_definition}");

  let code_entry = file_data
    .defs
    .get(resolved_definition.as_str())
    .expect("resolved definition exists");

  // Try to navigate to path, provide enhanced error message on failure
  let node = match navigate_to_path(&code_entry.code, &path) {
    Ok(n) => n,
    Err(original_error) => {
      // Find the longest valid path
      let mut valid_depth = 0;
      let mut current = &code_entry.code;

      for (depth, &idx) in path.iter().enumerate() {
        match current {
          Cirru::Leaf(_) => {
            valid_depth = depth;
            break;
          }
          Cirru::List(items) => {
            if idx >= items.len() {
              valid_depth = depth;
              break;
            }
            current = &items[idx];
            valid_depth = depth + 1;
          }
        }
      }

      // Get the node at the longest valid path
      let valid_path = &path[..valid_depth];
      let valid_node = navigate_to_path(&code_entry.code, valid_path).unwrap();

      // Format the valid path display
      let valid_path_display = format_path_bracketed(valid_path);

      // Get preview of the valid node
      let node_preview = match &valid_node {
        Cirru::Leaf(s) => format!("{:?} (leaf)", s.as_ref()),
        Cirru::List(items) => {
          let preview = valid_node.format_one_liner().unwrap_or_else(|_| "<complex>".to_string());
          let truncated = if preview.len() > 60 {
            format!("{}...", &preview[..60])
          } else {
            preview
          };
          format!("{} ({} items)", truncated, items.len())
        }
      };

      // Print enhanced error message
      eprintln!("{}", "Error: Invalid path".red().bold());
      eprintln!("{original_error}");
      eprintln!();
      eprintln!("{} Longest valid path: {}", "→".cyan(), valid_path_display.yellow());
      eprintln!("{} Node at that path: {}", "→".cyan(), node_preview.dimmed());
      eprintln!();

      // Show next steps based on node type
      match &valid_node {
        Cirru::Leaf(_) => {
          eprintln!("{} This is a leaf node (cannot navigate deeper)", "Note:".yellow().bold());
          eprintln!(
            "{} View it with: {}",
            "→".cyan(),
            format!("cr tree show '{}' -p '{}'", resolved_target, format_path(valid_path)).cyan()
          );
        }
        Cirru::List(items) => {
          eprintln!(
            "{} This node has {} children (indices 0-{})",
            "Available:".green().bold(),
            items.len(),
            items.len().saturating_sub(1)
          );
          eprintln!(
            "{} View it with: {}",
            "→".cyan(),
            format!("cr tree show '{}' -p '{}'", resolved_target, format_path(valid_path)).cyan()
          );

          // Show first few children as hints
          if !items.is_empty() {
            eprintln!();
            eprintln!("{} First few children:", "Hint:".blue().bold());
            for (i, item) in items.iter().enumerate().take(3) {
              let child_preview = format_child_preview(item);
              let child_path = if valid_path.is_empty() {
                i.to_string()
              } else {
                format!("{}.{}", format_path(valid_path), i)
              };
              eprintln!("  [{}] {} {} -p '{}'", i, child_preview.yellow(), "->".dimmed(), child_path);
            }
            if items.len() > 3 {
              eprintln!("  {}", format!("... and {} more", items.len() - 3).dimmed());
            }
          }
        }
      }

      return Err(String::new()); // Empty error since we already printed detailed message
    }
  };

  let node_type = match &node {
    Cirru::Leaf(_) => "leaf",
    Cirru::List(items) => {
      let chunk_options = ChunkDisplayOptions {
        trigger_nodes: opts.chunk_trigger_nodes,
        target_nodes: opts.chunk_target_nodes,
        max_nodes: opts.chunk_max_nodes,
        max_branches: 64,
      };
      let chunked_display = if opts.raw { None } else { maybe_chunk_node(&node, &chunk_options)? };

      let mut out = String::new();
      let _ = writeln!(&mut out, "{}: {} ({} items)", "Type".green().bold(), "list".yellow(), items.len());
      let _ = writeln!(&mut out);
      let shown_fragments = if let Some(display) = chunked_display {
        let (chunked, shown) = render_chunked_display(&display, opts.chunk_expand_depth);
        out.push_str(&chunked);
        Some((shown, display.fragments.len()))
      } else {
        let _ = writeln!(&mut out, "{}:", "Cirru preview".green().bold());
        let _ = writeln!(&mut out, "  ");
        let cirru_str = cirru_parser::format(std::slice::from_ref(&node), cirru_parser::CirruWriterOptions { use_inline: true })
          .map_err(|e| format!("Failed to format Cirru: {e}"))?;
        for line in cirru_str.lines() {
          let _ = writeln!(&mut out, "  {line}");
        }
        let _ = writeln!(&mut out);
        None
      };

      if show_json {
        let _ = writeln!(&mut out, "{}:", "JSON".green().bold());
        let _ = writeln!(&mut out, "{}", cirru_to_json(&node));
        if opts.depth > 0 {
          let _ = writeln!(&mut out, "{}", format!("(depth limited to {})", opts.depth).dimmed());
        }
        let _ = writeln!(&mut out);
      }

      let mut tips = Tips::new();
      if let Some((shown_fragments, total_fragments)) = shown_fragments {
        if shown_fragments < total_fragments {
          tips.add_with_priority(
            TipPriority::High,
            format!(
              "Showing ROOT plus {} chunk layer(s). Use {} to reveal deeper nested fragments, or {} to disable chunking.",
              opts.chunk_expand_depth,
              format!("--chunk-expand-depth {}", opts.chunk_expand_depth + 1).yellow(),
              "--raw".yellow()
            ),
          );
        }
      }
      tips.append(tip_prefer_oneliner_json(show_json));
      emit_cli_output(&out, render_to_stderr);
      tips.print();

      return Ok(());
    }
  };

  if matches!(node_type, "leaf") {
    let mut out = String::new();
    let _ = writeln!(&mut out, "{}: {}", "Type".green().bold(), "leaf".yellow());
    if let Cirru::Leaf(s) = &node {
      let _ = writeln!(&mut out, "{}: {:?}", "Value".green().bold(), s.as_ref());
      let _ = writeln!(&mut out);
      if command_guidance_enabled() {
        let _ = writeln!(
          &mut out,
          "{}: Use {} for symbols, {} for strings",
          "Tip".blue().bold(),
          "-e 'symbol'".yellow(),
          "-e '|text'".yellow()
        );
      }
    }
    emit_cli_output(&out, render_to_stderr);
  }

  Ok(())
}

fn handle_replace(opts: &TreeReplaceCommand, snapshot_file: &str) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;
  let path = parse_path(&opts.path)?;

  let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;

  let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;

  let new_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, true)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  // Save original for comparison
  let old_node = navigate_to_path(&code_entry.code, &path)?;
  // Tips: root-edit guidance
  if let Some(t) = tip_root_edit(path.is_empty()) {
    let mut tips = Tips::new();
    tips.add_with_priority(TipPriority::High, t);
    tips.print();
  }

  let new_code = apply_operation_at_path(&code_entry.code, &path, "replace", Some(&new_node))?;
  code_entry.code = new_code.clone();

  save_snapshot(&snapshot, snapshot_file)?;

  let replaced_node = navigate_to_path(&new_code, &path)?;

  println!("{} Replaced node", "✓".green());
  println!();
  println!("{}", "Changed node".blue().bold());
  print_preview_block("Before", &old_node, 20, "yellow");
  println!();
  print_preview_block("After", &replaced_node, 20, "green");

  if !path.is_empty() {
    let parent_path = &path[..path.len() - 1];
    let parent_after = if parent_path.is_empty() {
      new_code.clone()
    } else {
      navigate_to_path(&new_code, parent_path)?.clone()
    };
    println!();
    println!("{}", "Containing expression".blue().bold());
    print_preview_block("After", &parent_after, 12, "cyan");
  }

  Ok(())
}

fn handle_rewrite(opts: &TreeStructuralCommand, snapshot_file: &str) -> Result<(), String> {
  if opts.with.is_empty() {
    return Err("`tree rewrite` needs at least one `--with name=path`; for plain replacement use `tree replace`.".to_string());
  }

  let (namespace, definition) = parse_target(&opts.target)?;
  let path = parse_path(&opts.path)?;

  let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;

  let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;

  let new_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, true)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  let original_node = navigate_to_path(&code_entry.code, &path)?;
  let references = parse_with_references(&opts.with, &original_node)?;
  let processed_node = process_node_with_references(&new_node, &references)?;

  // Save original for comparison
  let old_node = navigate_to_path(&code_entry.code, &path)?;

  // Show diff preview
  println!("{}", show_diff_preview(&old_node, &processed_node, "rewrite"));
  // Tips: root-edit guidance
  if let Some(t) = tip_root_edit(path.is_empty()) {
    let mut tips = Tips::new();
    tips.add_with_priority(TipPriority::High, t);
    tips.print();
  }

  let new_code = apply_operation_at_path(&code_entry.code, &path, "replace", Some(&processed_node))?;
  code_entry.code = new_code.clone();

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Applied 'rewrite'", "✓".green());
  println!();
  println!("{}:", "From".yellow().bold());
  println!("{}", format_preview_with_type(&old_node, 20));
  println!();
  println!("{}:", "To".green().bold());
  let new_node = navigate_to_path(&new_code, &path)?;
  println!("{}", format_preview_with_type(&new_node, 20));
  println!();
  Ok(())
}

fn handle_replace_leaf(opts: &TreeReplaceLeafCommand, snapshot_file: &str) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;

  let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;
  let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;

  let replacement_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, true)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  // Find all leaf nodes that match the pattern
  let matches = find_all_leaf_matches(&code_entry.code, &opts.pattern, opts.regex, &[]);

  if matches.is_empty() {
    println!("{}", "No matches found.".yellow());
    return Ok(());
  }

  println!("{} {} match(es):", "Search:".bold(), matches.len());
  println!();

  // Show preview of matches
  for (i, (path, old_value)) in matches.iter().enumerate().take(20) {
    println!(
      "  {}. Path {}: {}",
      i + 1,
      format_path_bracketed(path).dimmed(),
      format!("{old_value:?}").yellow()
    );
  }

  if matches.len() > 20 {
    println!("  ... and {} more", matches.len() - 20);
  }
  println!();

  // Replace all matches (from end to beginning to maintain path validity)
  let mut new_code = code_entry.code.clone();
  let mut replaced_count = 0;

  // Sort paths in reverse order to replace from deepest/rightmost first
  let mut sorted_matches = matches.clone();
  sorted_matches.sort_by(|a, b| b.0.cmp(&a.0));

  for (path, _) in sorted_matches {
    match apply_operation_at_path(&new_code, &path, "replace", Some(&replacement_node)) {
      Ok(updated_code) => {
        new_code = updated_code;
        replaced_count += 1;
      }
      Err(e) => {
        eprintln!(
          "{} Failed to replace at path {}: {}",
          "Warning:".yellow(),
          format_path_bracketed(&path),
          e
        );
      }
    }
  }

  // Update the code entry
  code_entry.code = new_code;
  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Replaced {} occurrence(s)", "✓".green(), replaced_count);
  println!();
  println!("{}:", "Replacement".green().bold());
  println!(
    "  {} → {}",
    format!("{:?}", opts.pattern).yellow(),
    format_preview_with_type(&replacement_node, 0)
  );
  println!();
  Ok(())
}

fn handle_search_replace(opts: &TreeSearchReplaceCommand, snapshot_file: &str) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;

  let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;
  let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;

  let replacement_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, true)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  // Find all leaf nodes that match the pattern
  let matches = find_all_leaf_matches(&code_entry.code, &opts.pattern, opts.regex, &[]);

  if matches.is_empty() {
    return Err("No matches found for target pattern".to_string());
  }

  if matches.len() > 1 {
    println!("{} Found {} matches.", "Notice:".yellow().bold(), matches.len());
    println!("Please use specific path to replace:");
    println!();

    let replacement_arg = if let Some(c) = &opts.code {
      format!("-e '{c}'")
    } else if let Some(j) = &opts.json {
      format!("-j '{j}'")
    } else if let Some(f) = &opts.file {
      format!("-f '{f}'")
    } else {
      "-e '...'".to_string()
    };

    for (i, (path, _)) in matches.iter().enumerate().take(10) {
      let path_str = format_path(path);
      println!(
        "  {}. {} {} -p '{}' {}",
        i + 1,
        "cr tree replace".cyan(),
        opts.target,
        path_str,
        replacement_arg
      );
    }

    if matches.len() > 10 {
      println!("  ... and {} more", matches.len() - 10);
    }
    println!();
    if command_guidance_enabled() {
      println!("{}", "Tip: Use 'tree replace-leaf' if you want to replace ALL occurrences.".blue());
    }

    return Err(format!(
      "Found {} matches for pattern '{}'. Use a specific path with 'cr tree replace' to disambiguate (see suggestions above).",
      matches.len(),
      opts.pattern
    ));
  }

  // Exactly one match
  let (path, old_value) = &matches[0];
  let old_node = Cirru::Leaf(old_value.to_string().into());

  // Show diff preview
  println!("{}", show_diff_preview(&old_node, &replacement_node, "search-replace"));

  let new_code = apply_operation_at_path(&code_entry.code, path, "replace", Some(&replacement_node))?;
  code_entry.code = new_code;

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Replaced unique occurrence", "✓".green());

  Ok(())
}

/// Find leaf nodes matching the pattern; uses exact match or regex based on flag
fn find_all_leaf_matches(node: &Cirru, pattern: &str, regex: bool, current_path: &[usize]) -> Vec<(Vec<usize>, String)> {
  let mut results = Vec::new();

  // Compile regex once if needed
  let regex_pattern = if regex {
    match regex::Regex::new(pattern) {
      Ok(r) => Some(r),
      Err(e) => {
        eprintln!("{} Invalid regex '{}': {}", "Error:".red().bold(), pattern, e);
        return results;
      }
    }
  } else {
    None
  };

  match node {
    Cirru::Leaf(s) => {
      let matched = if regex {
        regex_pattern.as_ref().is_some_and(|r| r.is_match(s))
      } else {
        // Exact match (default)
        s.as_ref() == pattern
      };

      if matched {
        results.push((current_path.to_vec(), s.to_string()));
      }
    }
    Cirru::List(items) => {
      // Recursively search children
      for (i, item) in items.iter().enumerate() {
        let mut new_path = current_path.to_vec();
        new_path.push(i);
        results.extend(find_all_leaf_matches(item, pattern, regex, &new_path));
      }
    }
  }

  results
}

fn handle_delete(opts: &TreeDeleteCommand, snapshot_file: &str) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;
  let path = parse_path(&opts.path)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  // Save original node and parent for comparison
  let old_node = navigate_to_path(&code_entry.code, &path)?;
  let parent_path: Vec<usize> = if path.is_empty() { vec![] } else { path[..path.len() - 1].to_vec() };
  let old_parent = if parent_path.is_empty() {
    code_entry.code.clone()
  } else {
    navigate_to_path(&code_entry.code, &parent_path)?
  };

  // Show diff preview with parent context
  println!("\n{}: delete", "Preview".blue().bold());
  println!("{}:", "Node to delete".yellow().bold());
  println!("{}", format_preview_with_type(&old_node, 10));
  println!();
  println!("{}:", "Parent context".dimmed());
  println!("{}", format_preview_with_type(&old_parent, 8));
  println!();
  if let Some(t) = tip_root_edit(path.is_empty()) {
    let mut tips = Tips::new();
    tips.add_with_priority(TipPriority::High, t);
    tips.print();
  }

  let new_code = apply_operation_at_path(&code_entry.code, &path, "delete", None)?;
  code_entry.code = new_code.clone();

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Deleted node", "✓".green());
  println!();
  println!("{}:", "Deleted node".yellow().bold());
  println!("{}", format_preview_with_type(&old_node, 20));
  println!();
  println!("{}:", "Parent after deletion".green().bold());
  let new_parent = if parent_path.is_empty() {
    new_code.clone()
  } else {
    navigate_to_path(&new_code, &parent_path)?
  };
  println!("{}", format_preview_with_type(&new_parent, 20));
  println!();

  // Warn about index changes
  if !path.is_empty() {
    let deleted_index = path[path.len() - 1];
    println!(
      "{}: Sibling nodes after index {} have shifted down by 1",
      "⚠️  Index change".yellow().bold(),
      deleted_index
    );
    println!(
      "   Example: path {}.{} is now {}.{}",
      format_path(&parent_path),
      deleted_index + 1,
      format_path(&parent_path),
      deleted_index
    );
    println!(
      "   {}: Re-run {} to get updated paths",
      "Tip".blue().bold(),
      "cr query search".cyan()
    );
  }

  Ok(())
}

fn handle_batch_delete(opts: &TreeBatchDeleteCommand, snapshot_file: &str) -> Result<(), String> {
  // First, parse and validate all paths, sorting by path descending
  // so we delete from highest index to lowest, avoiding index drift
  let mut parsed: Vec<(Vec<usize>, String)> = Vec::new();
  for p in &opts.paths {
    let path = parse_path(p).map_err(|e| format!("Invalid path '{p}': {e}"))?;
    parsed.push((path, p.clone()));
  }
  // Sort by path descending
  parsed.sort_by(|a, b| b.0.cmp(&a.0));

  for (_, original_path) in &parsed {
    let delete_opts = TreeDeleteCommand {
      target: opts.target.clone(),
      path: original_path.clone(),
      depth: opts.depth,
    };
    handle_delete(&delete_opts, snapshot_file)?;
    println!();
  }

  println!("{} Deleted {} path(s) via batch-delete", "✓".green(), parsed.len());
  Ok(())
}

fn handle_insert_before(opts: &TreeInsertBeforeCommand, snapshot_file: &str) -> Result<(), String> {
  generic_insert_handler(&opts.target, &opts.path, "insert-before", opts, snapshot_file, opts.depth)
}

fn handle_insert_after(opts: &TreeInsertAfterCommand, snapshot_file: &str) -> Result<(), String> {
  generic_insert_handler(&opts.target, &opts.path, "insert-after", opts, snapshot_file, opts.depth)
}

fn handle_insert_child(opts: &TreeInsertChildCommand, snapshot_file: &str) -> Result<(), String> {
  generic_insert_handler(&opts.target, &opts.path, "insert-child", opts, snapshot_file, opts.depth)
}

fn handle_append_child(opts: &TreeAppendChildCommand, snapshot_file: &str) -> Result<(), String> {
  generic_insert_handler(&opts.target, &opts.path, "append-child", opts, snapshot_file, opts.depth)
}

// Generic trait for insert-like operations
trait InsertOperation {
  fn file(&self) -> &Option<String>;
  fn code(&self) -> &Option<String>;
  fn json(&self) -> &Option<String>;
  fn json_input(&self) -> bool;
  fn with(&self) -> &[String] {
    &[]
  }
}

impl InsertOperation for TreeInsertBeforeCommand {
  fn file(&self) -> &Option<String> {
    &self.file
  }
  fn code(&self) -> &Option<String> {
    &self.code
  }
  fn json(&self) -> &Option<String> {
    &self.json
  }
  fn json_input(&self) -> bool {
    self.json_input
  }
}

impl InsertOperation for TreeInsertAfterCommand {
  fn file(&self) -> &Option<String> {
    &self.file
  }
  fn code(&self) -> &Option<String> {
    &self.code
  }
  fn json(&self) -> &Option<String> {
    &self.json
  }
  fn json_input(&self) -> bool {
    self.json_input
  }
}

impl InsertOperation for TreeInsertChildCommand {
  fn file(&self) -> &Option<String> {
    &self.file
  }
  fn code(&self) -> &Option<String> {
    &self.code
  }
  fn json(&self) -> &Option<String> {
    &self.json
  }
  fn json_input(&self) -> bool {
    self.json_input
  }
}

impl InsertOperation for TreeAppendChildCommand {
  fn file(&self) -> &Option<String> {
    &self.file
  }
  fn code(&self) -> &Option<String> {
    &self.code
  }
  fn json(&self) -> &Option<String> {
    &self.json
  }
  fn json_input(&self) -> bool {
    self.json_input
  }
}

impl InsertOperation for TreeSearchReplaceCommand {
  fn file(&self) -> &Option<String> {
    &self.file
  }
  fn code(&self) -> &Option<String> {
    &self.code
  }
  fn json(&self) -> &Option<String> {
    &self.json
  }
  fn json_input(&self) -> bool {
    self.json_input
  }
}

impl InsertOperation for TreeStructuralCommand {
  fn file(&self) -> &Option<String> {
    &self.file
  }
  fn code(&self) -> &Option<String> {
    &self.code
  }
  fn json(&self) -> &Option<String> {
    &self.json
  }
  fn json_input(&self) -> bool {
    self.json_input
  }
  fn with(&self) -> &[String] {
    &self.with
  }
}

fn generic_insert_handler<T: InsertOperation>(
  target: &str,
  path_str: &str,
  operation: &str,
  opts: &T,
  snapshot_file: &str,
  _depth: usize,
) -> Result<(), String> {
  let (namespace, definition) = parse_target(target)?;
  let path = parse_path(path_str)?;

  let code_input = read_code_input(opts.file(), opts.code(), opts.json())?;

  let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;

  let new_node = parse_input_to_cirru(raw, opts.json(), opts.json_input(), true)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  let processed_node = if opts.with().is_empty() {
    new_node
  } else {
    let original_node = navigate_to_path(&code_entry.code, &path)?;
    let references = parse_with_references(opts.with(), &original_node)?;
    process_node_with_references(&new_node, &references)?
  };

  // Save parent before insertion for comparison
  let parent_path: Vec<usize> = if path.is_empty() { vec![] } else { path[..path.len() - 1].to_vec() };
  let old_parent = if parent_path.is_empty() {
    code_entry.code.clone()
  } else {
    navigate_to_path(&code_entry.code, &parent_path)?
  };

  // Show diff preview
  println!("\n{}: {}", "Preview".blue().bold(), operation);
  println!("{}:", "Node to insert".cyan().bold());
  println!("{}", format_preview_with_type(&processed_node, 8));
  println!();
  println!("{}:", "Parent before".dimmed());
  println!("{}", format_preview_with_type(&old_parent, 8));
  println!();
  if let Some(t) = tip_root_edit(path.is_empty()) {
    let mut tips = Tips::new();
    tips.add_with_priority(TipPriority::High, t);
    tips.print();
  }

  let new_code = apply_operation_at_path(&code_entry.code, &path, operation, Some(&processed_node))?;
  code_entry.code = new_code.clone();

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Applied '{}'", "✓".green(), operation);
  println!();
  println!("{}:", "Inserted node".cyan().bold());
  println!("{}", format_preview_with_type(&processed_node, 10));
  println!();
  println!("{}:", "Parent before".yellow().bold());
  println!("{}", format_preview_with_type(&old_parent, 15));
  println!();
  println!("{}:", "Parent after".green().bold());
  let new_parent = if parent_path.is_empty() {
    new_code.clone()
  } else {
    navigate_to_path(&new_code, &parent_path)?
  };
  println!("{}", format_preview_with_type(&new_parent, 15));
  println!();

  // Explain index impact based on operation
  match operation {
    "insert-before" => {
      if !path.is_empty() {
        let insert_index = path[path.len() - 1];
        println!(
          "{}: Node inserted at index {}, original node and siblings shifted up by 1",
          "Index impact".yellow().bold(),
          insert_index
        );
        println!(
          "   Old path {}.{} → New path {}.{}",
          format_path(&parent_path),
          insert_index,
          format_path(&parent_path),
          insert_index + 1
        );
      }
    }
    "insert-after" => {
      if !path.is_empty() {
        let ref_index = path[path.len() - 1];
        println!(
          "{}: Node inserted at index {}, nodes after reference shifted up by 1",
          "Index impact".yellow().bold(),
          ref_index + 1
        );
      }
    }
    "insert-child" => {
      println!(
        "{}: Node inserted as first child (index 0), all existing children shifted up by 1",
        "Index impact".yellow().bold()
      );
      println!("   Old child [0] → New child [1], [1] → [2], etc.");
    }
    "append-child" => {
      println!(
        "{}: Node appended as last child, no index changes to existing nodes",
        "Index impact".green().bold()
      );
      println!("   {}:  Use this for multiple insertions to keep paths stable", "Tip".blue().bold());
    }
    _ => {}
  }

  Ok(())
}

fn handle_swap_next(opts: &TreeSwapNextCommand, snapshot_file: &str) -> Result<(), String> {
  generic_swap_handler(&opts.target, &opts.path, "swap-next-sibling", snapshot_file, opts.depth)
}

fn handle_swap_prev(opts: &TreeSwapPrevCommand, snapshot_file: &str) -> Result<(), String> {
  generic_swap_handler(&opts.target, &opts.path, "swap-prev-sibling", snapshot_file, opts.depth)
}

fn generic_swap_handler(target: &str, path_str: &str, operation: &str, snapshot_file: &str, _depth: usize) -> Result<(), String> {
  let (namespace, definition) = parse_target(target)?;
  let path = parse_path(path_str)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  // Save parent before swap for comparison
  let parent_path: Vec<usize> = if path.is_empty() { vec![] } else { path[..path.len() - 1].to_vec() };
  let old_parent = if parent_path.is_empty() {
    code_entry.code.clone()
  } else {
    navigate_to_path(&code_entry.code, &parent_path)?
  };

  let new_code = apply_operation_at_path(&code_entry.code, &path, operation, None)?;
  code_entry.code = new_code.clone();

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Applied '{}'", "✓".green(), operation);
  println!();

  // Explain what was swapped
  if !path.is_empty() {
    let current_index = path[path.len() - 1];
    let parent_display = if parent_path.is_empty() {
      "root".to_string()
    } else {
      format!("[{}]", parent_path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join("."))
    };

    match operation {
      "swap-next-sibling" => {
        println!(
          "{}: Swapped child [{}] with [{}] under parent {}",
          "Index change".yellow().bold(),
          current_index,
          current_index + 1,
          parent_display
        );
      }
      "swap-prev-sibling" => {
        println!(
          "{}: Swapped child [{}] with [{}] under parent {}",
          "Index change".yellow().bold(),
          current_index,
          current_index - 1,
          parent_display
        );
      }
      _ => {}
    }
    println!();
  }

  println!("{}:", "Parent before swap".yellow().bold());
  println!("{}", format_preview_with_type(&old_parent, 15));
  println!();
  if let Some(t) = tip_root_edit(path.is_empty()) {
    let mut tips = Tips::new();
    tips.add_with_priority(TipPriority::High, t);
    tips.print();
  }
  println!("{}:", "Parent after swap".green().bold());
  let new_parent = if parent_path.is_empty() {
    new_code.clone()
  } else {
    navigate_to_path(&new_code, &parent_path)?
  };
  println!("{}", format_preview_with_type(&new_parent, 15));

  Ok(())
}

/// Splice all children of the node at `path` into the parent list, removing the wrapper node.
/// e.g. parent `(foo (a b c) d)` with unwrap of `(a b c)` gives `(foo a b c d)`.
fn splice_at_path(code: &Cirru, path: &[usize]) -> Result<Cirru, String> {
  if path.is_empty() {
    return Err("Cannot unwrap root node (no parent to splice into)".to_string());
  }
  splice_recursive(code, path, 0)
}

fn splice_recursive(code: &Cirru, path: &[usize], depth: usize) -> Result<Cirru, String> {
  match code {
    Cirru::Leaf(_) => Err(format!("Cannot navigate into leaf node at depth {depth}")),
    Cirru::List(items) => {
      let idx = path[depth];
      if idx >= items.len() {
        return Err(format!("Path index {} out of bounds (list has {} items)", idx, items.len()));
      }
      if depth == path.len() - 1 {
        // At parent level: splice children of items[idx] in place
        let splice_children = match &items[idx] {
          Cirru::List(children) => children.clone(),
          Cirru::Leaf(_) => return Err("Node at path is a leaf; cannot unwrap".to_string()),
        };
        let mut new_items: Vec<Cirru> = Vec::with_capacity(items.len() - 1 + splice_children.len());
        new_items.extend_from_slice(&items[..idx]);
        new_items.extend(splice_children);
        new_items.extend_from_slice(&items[idx + 1..]);
        Ok(Cirru::List(new_items))
      } else {
        let mut new_items = items.clone();
        new_items[idx] = splice_recursive(&items[idx], path, depth + 1)?;
        Ok(Cirru::List(new_items))
      }
    }
  }
}

fn handle_unwrap(opts: &TreeUnwrapCommand, snapshot_file: &str) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;
  let path = parse_path(&opts.path)?;

  if path.is_empty() {
    return Err("Cannot unwrap root node (no parent to splice into)".to_string());
  }

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  let node = navigate_to_path(&code_entry.code, &path)?;
  let children = match &node {
    Cirru::List(children) => children.clone(),
    _ => return Err(format!("Node at path [{}] is a leaf; cannot unwrap", opts.path)),
  };

  if children.is_empty() {
    return Err(format!("Node at path [{}] has no children to splice", opts.path));
  }

  println!("\n{}: unwrap {} child(ren)", "Preview".blue().bold(), children.len());
  println!("{}:", "Before".dimmed());
  println!("{}", format_preview_with_type(&node, opts.depth));
  println!("{} (spliced):", "After".cyan().bold());
  for (i, child) in children.iter().enumerate() {
    println!("  [{}] {}", i, format_preview_with_type(child, opts.depth));
  }
  println!();

  let new_code = splice_at_path(&code_entry.code, &path)?;
  code_entry.code = new_code;

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Unwrapped node", "✓".green());

  Ok(())
}

fn handle_raise(opts: &TreeRaiseCommand, snapshot_file: &str) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;
  let path = parse_path(&opts.path)?;

  if path.is_empty() {
    return Err("Cannot raise root node (no parent to replace). Path must have at least one element.".to_string());
  }

  let parent_path = &path[..path.len() - 1];

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  let child_node = navigate_to_path(&code_entry.code, &path)?.clone();
  let parent_node = navigate_to_path(&code_entry.code, parent_path)?;

  println!("\n{}: raise", "Preview".blue().bold());
  println!("{}:", "Before (parent)".dimmed());
  println!("{}", format_preview_with_type(&parent_node, opts.depth));
  println!("{}:", "After (raised child)".cyan().bold());
  println!("{}", format_preview_with_type(&child_node, opts.depth));
  println!();

  let new_code = apply_operation_at_path(&code_entry.code, parent_path, "replace", Some(&child_node))?;
  code_entry.code = new_code;

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Raised node", "✓".green());

  Ok(())
}

fn handle_wrap(opts: &TreeWrapCommand, snapshot_file: &str) -> Result<(), String> {
  let (namespace, definition) = parse_target(&opts.target)?;
  let path = parse_path(&opts.path)?;

  let raw = read_code_input(&opts.file, &opts.code, &opts.json)?.ok_or(ERR_CODE_INPUT_REQUIRED)?;
  let auto_json = opts.code.is_some();
  let template = parse_input_to_cirru(&raw, &opts.json, opts.json_input, auto_json)?;

  let mut snapshot = load_snapshot(snapshot_file)?;
  check_ns_editable(&snapshot, namespace)?;

  let file_data = snapshot
    .files
    .get_mut(namespace)
    .ok_or_else(|| format!("Namespace '{namespace}' not found"))?;

  let code_entry = file_data
    .defs
    .get_mut(definition)
    .ok_or_else(|| format!("Definition '{definition}' not found"))?;

  let original_node = navigate_to_path(&code_entry.code, &path)?.clone();

  // `self` always refers to the original node (equivalent to rewrite --with self=.)
  let mut references = std::collections::BTreeMap::new();
  references.insert("self".to_string(), original_node.clone());

  let new_node = process_node_with_references(&template, &references)?;

  println!("\n{}: wrap", "Preview".blue().bold());
  println!("{}:", "Before".dimmed());
  println!("{}", format_preview_with_type(&original_node, opts.depth));
  println!("{}:", "After".cyan().bold());
  println!("{}", format_preview_with_type(&new_node, opts.depth));
  println!();

  let new_code = apply_operation_at_path(&code_entry.code, &path, "replace", Some(&new_node))?;
  code_entry.code = new_code;

  save_snapshot(&snapshot, snapshot_file)?;

  println!("{} Wrapped node", "✓".green());

  Ok(())
}
