///|
/// Traverse all nodes in module index
pub fn traverse_module_index(
  index : IndexNode,
  f : (IndexNode, String) -> Unit,
) -> Unit {
  fn go(node : IndexNode, current_path : String) {
    f(node, current_path)
    for child in node.childs {
      go(child, "\{current_path}/\{child.name}")
    }
  }

  go(index, index.name)
}

///|
/// Get all package nodes from module index
pub fn get_package_nodes(index : IndexNode) -> Array[(PackageIndex, String)] {
  let result : Array[(PackageIndex, String)] = []
  traverse_module_index(index, fn(node, path) {
    match node.pkg {
      Some(p) => result.push((p, path))
      None => ()
    }
  })
  result
}

///|
/// Get all package paths from module index
pub fn get_all_package_paths(index : IndexNode) -> Array[String] {
  let paths : Array[String] = []
  for pkg_path in get_package_nodes(index) {
    paths.push(pkg_path.0.path)
  }
  paths
}

///|
/// Find a node by path in module index
pub fn find_node_by_path(index : IndexNode, path : String) -> IndexNode? {
  let parts : Array[String] = path
    .split("/")
    .map(fn(sv) { sv.to_string() })
    .collect()
  if parts.length() == 0 {
    return None
  }
  if parts[0] != index.name {
    return None
  }
  fn go(node : IndexNode, start_idx : Int, arr : Array[String]) -> IndexNode? {
    if start_idx >= arr.length() {
      return Some(node)
    }
    let target = arr[start_idx]
    for child in node.childs {
      if child.name == target {
        return go(child, start_idx + 1, arr)
      }
    }
    None
  }

  go(index, 1, parts)
}

///|
/// Get all descendant packages of a node (not just direct children)
pub fn get_descendant_packages(node : IndexNode) -> Array[PackageIndex] {
  let packages : Array[PackageIndex] = []
  fn go(n : IndexNode) {
    match n.pkg {
      Some(p) => packages.push(p)
      None => ()
    }
    for child in n.childs {
      go(child)
    }
  }

  go(node)
  packages
}

///|
/// Get direct child packages of a node
pub fn get_child_packages(node : IndexNode) -> Array[PackageIndex] {
  let packages : Array[PackageIndex] = []
  for child in node.childs {
    match child.pkg {
      Some(p) => packages.push(p)
      None => ()
    }
  }
  packages
}

///|
/// Build a map of nodes by path
pub fn build_node_map(index : IndexNode) -> Map[String, IndexNode] {
  let map : Map[String, IndexNode] = {}
  traverse_module_index(index, fn(node, path) { map[path] = node })
  map
}

///|
/// Convert TypePath to string
pub fn type_path_to_string(tp : TypePath) -> String {
  if tp.path.is_empty() {
    tp.name
  } else {
    "@\{tp.path}.\{tp.name}"
  }
}

///|
/// Convert Stype to string
pub fn stype_to_string(stype : Stype) -> String {
  match stype {
    Stype::Constr(constr~, arguments~) => {
      let name = type_path_to_string(constr)
      if arguments.length() == 0 {
        name
      } else {
        let args = arguments.map(stype_to_string).join(", ")
        "\{name}[\{args}]"
      }
    }
    Stype::Arrow(parameters~, return_type~, error_type~, is_async~) => {
      let params = parameters.map(stype_to_string).join(", ")
      let ret = stype_to_string(return_type)
      let prefix = if is_async { "async " } else { "" }
      let error_suffix = match error_type {
        Some(e) => " raise(\{stype_to_string(e)})"
        None => ""
      }
      "\{prefix}(\{params}) -> \{ret}\{error_suffix}"
    }
    Stype::Param(name~) => name
    Stype::Path(path~, name~) =>
      if path.is_empty() {
        name
      } else {
        "@\{path}.\{name}"
      }
  }
}

///|
/// Build search entries from package data
pub fn build_search_entries(
  pkg : PackageData,
  path : String,
) -> Array[SearchEntry] {
  let entries : Array[SearchEntry] = []

  // Add types
  for t in pkg.types {
    entries.push(SearchEntry::{
      name: t.name,
      path,
      kind: "type",
      signature: Some(t.signature),
    })
    // Add type methods
    for m in t.methods {
      entries.push(SearchEntry::{
        name: "\{t.name}::\{m.name}",
        path,
        kind: "method",
        signature: Some(m.signature),
      })
    }
  }

  // Add traits
  for t in pkg.traits {
    entries.push(SearchEntry::{
      name: t.name,
      path,
      kind: "trait",
      signature: Some(t.signature),
    })
  }

  // Add type aliases
  for t in pkg.type_aliases {
    entries.push(SearchEntry::{
      name: t.name,
      path,
      kind: "typealias",
      signature: Some(t.signature),
    })
  }

  // Add values
  for v in pkg.values {
    entries.push(SearchEntry::{
      name: v.name,
      path,
      kind: "value",
      signature: Some(v.signature),
    })
  }

  // Add errors
  for e in pkg.errors {
    entries.push(SearchEntry::{
      name: e.name,
      path,
      kind: "error",
      signature: Some(e.signature),
    })
  }
  entries
}

///|
/// Build all search entries from module index
pub fn build_all_search_entries(
  index : IndexNode,
  packages : Map[String, PackageData],
) -> Array[SearchEntry] {
  let entries : Array[SearchEntry] = []
  for path in get_all_package_paths(index) {
    match packages.get(path) {
      Some(pkg) => entries.append(build_search_entries(pkg, path))
      None => ()
    }
  }
  entries
}

///|
/// Simple fuzzy search (case-insensitive substring match)
pub fn fuzzy_search(
  entries : Array[SearchEntry],
  query : String,
  limit : Int,
) -> Array[SearchEntry] {
  let query_lower = query.to_lower()
  let results : Array[SearchEntry] = []
  for entry in entries {
    if results.length() >= limit {
      break
    }
    if entry.name.to_lower().contains(query_lower) {
      results.push(entry)
    }
  }
  results
}

///|
/// Strip HTML tags from string
pub fn strip_html_tags(s : String) -> String {
  let result = StringBuilder::new()
  let mut in_tag = false
  for c in s {
    if c == '<' {
      in_tag = true
    } else if c == '>' {
      in_tag = false
    } else if not(in_tag) {
      result.write_char(c)
    }
  }
  result.to_string()
}

///|
/// Format docstring (strip HTML and trim)
pub fn format_docstring(docstring : String) -> String {
  strip_html_tags(docstring).trim(chars=" \t\n\r").to_string()
}