// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Import Resolution for diago IR
///
/// Handles file import statements.
/// Imports are resolved by a user-provided resolver function.

///|
/// Import resolver function type
/// Takes an import path and returns the file content, or None if not found
pub struct ImportResolver((String) -> String?)

///|
/// Create a new import resolver from a function
pub fn ImportResolver::new(f : (String) -> String?) -> ImportResolver {
  ImportResolver(f)
}

///|
/// Compile an AST Map with import resolution
pub fn compile_with_imports(
  ast : @ast.Map,
  resolver : ImportResolver,
) -> Map raise CompileError {
  let ir = Map::from_ast(ast)
  compile_map_non_glob_nodes_with_imports(ir, ast, ast.nodes, [], resolver, [])
  resolve_boards_with_imports(ir, resolver)
  compile_map_glob_nodes_with_imports(ir, ast, ast.nodes, [], resolver, [])
  // Resolve variable substitutions
  ir.resolve_substitutions()
  apply_suspensions(ir)
  ir.reindex_paths()
}

///|
fn compile_map_nodes_with_imports(
  ir : Map,
  scope_ast : @ast.Map,
  nodes : Array[@ast.MapNode],
  scope_frames : Array[CompileScopeFrame],
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path? : String? = None,
) -> Unit raise CompileError {
  compile_map_non_glob_nodes_with_imports(
    ir,
    scope_ast,
    nodes,
    scope_frames,
    resolver,
    import_stack,
    source_path~,
  )
  compile_map_glob_nodes_with_imports(
    ir,
    scope_ast,
    nodes,
    scope_frames,
    resolver,
    import_stack,
    source_path~,
  )
}

///|
fn compile_map_non_glob_nodes_with_imports(
  ir : Map,
  scope_ast : @ast.Map,
  nodes : Array[@ast.MapNode],
  scope_frames : Array[CompileScopeFrame],
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path? : String? = None,
) -> Unit raise CompileError {
  for node in nodes {
    match node {
      Comment(_, _) => ()
      BlockComment(_, _) => ()
      Substitution(sub) =>
        ir.add_field(
          Field::new("", Some(substitution_scalar(sub, source_path)), None, []),
        )
      Import(imp) => compile_import(ir, imp, resolver, import_stack)
      Key(key) =>
        if !key_uses_glob(key) {
          compile_key_with_imports(
            ir, key, scope_ast, scope_frames, resolver, import_stack, source_path,
          )
        }
    }
  }
}

///|
fn compile_map_glob_nodes_with_imports(
  ir : Map,
  scope_ast : @ast.Map,
  nodes : Array[@ast.MapNode],
  scope_frames : Array[CompileScopeFrame],
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path? : String? = None,
) -> Unit raise CompileError {
  for node in nodes {
    match node {
      Key(key) =>
        if key_uses_glob(key) {
          compile_key_with_imports(
            ir, key, scope_ast, scope_frames, resolver, import_stack, source_path,
          )
        }
      _ => ()
    }
  }
}

///|
/// Compile an import statement
fn compile_import(
  ir : Map,
  imp : @ast.Import,
  resolver : ImportResolver,
  import_stack : Array[String],
) -> Unit raise CompileError {
  let (_, _, display) = split_import_file_and_selector(imp)
  let (import_ir, selector) = compile_import_target(imp, resolver, import_stack)
  if imp.spread {
    // Spread import in map context: only maps are allowed.
    let m2 = select_map_for_spread(import_ir, selector) catch {
      _ => raise ImportError("cannot spread import non map into map")
    }
    merge_into_map(ir, m2)
    return
  }

  // Non-spread import at map node level: create field named by the imported file key.
  let (primary, composite) = select_primary_and_composite(import_ir, selector)
  let field_name = extract_filename(display)
  let field0 = ir.ensure_field(field_name)
  ir.set_field(
    Field::new(
      field0.name,
      primary,
      composite,
      field0.references,
      name_syntax=field0.name_syntax(),
    ),
  )
}

///|
fn compile_import_target(
  imp : @ast.Import,
  resolver : ImportResolver,
  import_stack : Array[String],
) -> (Map, Array[@ast.StringValue]) raise CompileError {
  let (file_key, selector, display) = split_import_file_and_selector(imp)
  let (content, canonical) = resolve_import_content(resolver, file_key) catch {
    _ => raise ImportError("could not resolve import '\{display}'")
  }
  if contains(import_stack, canonical) {
    raise ImportError(
      "detected cyclic import chain: \{format_cycle(import_stack, canonical)}",
    )
  }
  let stack2 = import_stack.copy()
  stack2.push(canonical)
  let (import_ast, errors) = @parser.parse(content)
  if !errors.is_empty() {
    raise ImportError("parse error in imported file '\{display}': \{errors}")
  }
  let import_ir = Map::from_ast(import_ast, source_path=Some(canonical))
  compile_map_nodes_with_imports(
    import_ir,
    import_ast,
    import_ast.nodes,
    [],
    resolver,
    stack2,
    source_path=Some(canonical),
  )
  (with_import_ast_map(import_ir, Import(imp)), selector)
}

///|
fn split_import_file_and_selector(
  imp : @ast.Import,
) -> (String, Array[@ast.StringValue], String) {
  // `@a.b.c` means: import file "a", then select path ["b", "c"].
  // The resolver key is the file part only.
  let file = if imp.path.length() > 0 {
    join_import_pre(imp.pre, imp.path[0].content())
  } else {
    join_import_pre(imp.pre, "")
  }
  let selector : Array[@ast.StringValue] = []
  for i = 1; i < imp.path.length(); i = i + 1 {
    selector.push(imp.path[i])
  }
  let display = if selector.length() == 0 {
    file
  } else {
    let selector_parts : Array[String] = []
    for part in selector {
      selector_parts.push(part.content())
    }
    file + "." + selector_parts.join(".")
  }
  (file, selector, display)
}

///|
fn join_import_pre(pre : String, file : String) -> String {
  let combined = pre + file
  let is_abs = combined.length() > 0 && combined.to_array()[0] == '/'
  let parts : Array[String] = []
  for seg in combined.split("/").collect() {
    let s = seg.to_owned()
    if s == "" || s == "." {
      continue
    }
    if s == ".." {
      if parts.length() > 0 && parts[parts.length() - 1] != ".." {
        let _ = parts.pop()
      } else if !is_abs {
        parts.push("..")
      }
      continue
    }
    parts.push(s)
  }
  let joined = parts.join("/")
  if is_abs {
    if joined == "" {
      "/"
    } else {
      "/" + joined
    }
  } else {
    joined
  }
}

///|
fn resolve_import_content(
  resolver : ImportResolver,
  file_key : String,
) -> (String, String) raise CompileError {
  match (resolver.0)(file_key) {
    Some(content) => (content, file_key)
    None =>
      if file_key.has_suffix(".d2") {
        raise ImportError("missing import")
      } else {
        let k2 = file_key + ".d2"
        match (resolver.0)(k2) {
          Some(content) => (content, k2)
          None => raise ImportError("missing import")
        }
      }
  }
}

///|
fn contains(arr : Array[String], s : String) -> Bool {
  for x in arr {
    if x == s {
      return true
    }
  }
  false
}

///|
fn format_cycle(stack : Array[String], next : String) -> String {
  let parts = stack.copy()
  parts.push(next)
  parts.join(" -> ")
}

///|
fn select_primary_and_composite(
  root : Map,
  selector : Array[@ast.StringValue],
) -> (Scalar?, Composite?) raise CompileError {
  if selector.length() == 0 {
    return (None, Some(Map(root)))
  }
  let f = select_field(root, selector)
  (f.primary, f.composite)
}

///|
fn select_map_for_spread(
  root : Map,
  selector : Array[@ast.StringValue],
) -> Map raise CompileError {
  if selector.length() == 0 {
    return root
  }
  let f = select_field(root, selector)
  match f.composite {
    Some(Map(m)) => m
    _ => raise ImportError("not a map")
  }
}

///|
fn select_field(
  root : Map,
  selector : Array[@ast.StringValue],
) -> Field raise CompileError {
  let mut current = root
  for i = 0; i < selector.length(); i = i + 1 {
    match current.get_field_by_syntax(selector[i]) {
      Some(f) =>
        if i == selector.length() - 1 {
          return f
        } else {
          match f.composite {
            Some(Map(m)) => current = m
            _ => raise ImportError("not a map")
          }
        }
      None => raise ImportError("not found")
    }
  }
  raise ImportError("not found")
}

///|
fn merge_into_map(dst : Map, src : Map) -> Unit {
  for f in src.fields {
    dst.set_field(f)
  }
  for e in src.edges {
    dst.set_edge(e)
  }
}

///|
/// Extract filename without extension from path
fn extract_filename(path : String) -> String {
  // Find last /
  let mut last_slash = -1
  for i in 0.. Unit raise CompileError {
  // Handle ampersand filters
  if key.ampersand || key.not_ampersand {
    return
  }
  // Handle edges first
  if key.edges.length() > 0 {
    compile_edges_with_imports(
      ir, key, scope_ast, scope_frames, resolver, import_stack, source_path,
    )
    return
  }
  // Handle regular key
  guard key.key is Some(key_path) else { return }
  // Check for glob pattern
  if key_path.has_glob() {
    compile_glob_key_with_imports(
      ir, key_path, key, scope_ast, resolver, import_stack, source_path,
    )
    return
  }
  let context = RefContext::new(None, Some(key), Some(scope_ast), source_path~)
  let path = key_path.to_strings()
  if path.length() == 0 {
    return
  }
  let parent_count = leading_parent_count(key_path)
  if parent_count > scope_frames.length() {
    raise InvalidKey("invalid underscore: no parent")
  }
  if parent_count == path.length() {
    raise InvalidKey("field key must contain more than underscores")
  }
  // Navigate/create the field path
  let mut current_map = if parent_count == 0 {
    ir
  } else {
    scope_frames[scope_frames.length() - parent_count].parent
  }
  let current_frames : Array[CompileScopeFrame] = []
  for i = 0; i < scope_frames.length() - parent_count; i = i + 1 {
    current_frames.push(scope_frames[i])
  }
  for i = parent_count; i < path.length() - 1; i = i + 1 {
    let elem = key_path.path[i]
    let field = record_field_reference(
      current_map,
      current_map.ensure_field_by_syntax(elem),
      elem,
      key_path,
      context,
      i,
      false,
    )
    current_frames.push(CompileScopeFrame::new(current_map, elem))
    match field.composite {
      Some(Map(m)) => current_map = m
      None => {
        let new_map = Map::new()
        let field = Field::new(
          field.name,
          field.primary,
          Some(Map(new_map)),
          field.references,
          name_syntax=field.name_syntax(),
        )
        current_map.set_field(field)
        current_map = new_map
      }
      Some(_) => raise InvalidKey("field already has non-map value")
    }
  }
  // Set the final field
  let last_index = path.length() - 1
  let last_elem = key_path.path[last_index]
  let field0 = current_map.ensure_field_by_syntax(last_elem)
  let field1 = if key.primary is Some(_) || key.value is None {
    record_field_reference(
      current_map,
      field0,
      last_elem,
      key_path,
      context,
      last_index,
      key.primary is Some(_),
    )
  } else {
    field0
  }
  let field = if key.value is Some(_) {
    record_field_reference(
      current_map, field1, last_elem, key_path, context, last_index, true,
    )
  } else {
    field1
  }
  if key_suspension(key) is Some(_) {
    current_map.set_field(field)
    return
  }
  // Set primary value
  let field = match key.primary {
    Some(scalar) =>
      Field::new(
        field.name,
        Some(compile_scalar(scalar, source_path)),
        field.composite,
        field.references,
        name_syntax=field.name_syntax(),
      )
    None => field
  }
  // Set value (with import handling)
  let field = match key.value {
    Some(value) =>
      compile_value_with_imports(
        field, value, current_map, current_frames, resolver, import_stack, source_path,
      )
    None => field
  }
  current_map.set_field(field)
}

///|
fn compile_glob_key_with_imports(
  ir : Map,
  key_path : @ast.KeyPath,
  key : @ast.Key,
  scope_ast : @ast.Map,
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path : String?,
) -> Unit raise CompileError {
  let (pattern, glob_index) = extract_glob_and_path(key_path)
  if pattern.length() == 0 || glob_index < 0 {
    return
  }
  let filters = collect_filters_from_value(key)
  let suspension = key_suspension(key)
  let properties = if glob_index == key_path.path.length() - 1 {
    let properties = Map::new()
    match key.value {
      Some(Map(value_map)) =>
        for node in value_map.nodes {
          match node {
            Key(property_key) =>
              if !property_key.ampersand &&
                !property_key.not_ampersand &&
                !key_uses_glob(property_key) {
                compile_key_with_imports(
                  properties,
                  property_key,
                  value_map,
                  [],
                  resolver,
                  import_stack,
                  source_path,
                )
              }
            _ => ()
          }
        }
      _ => ()
    }
    properties
  } else {
    let context = RefContext::new(
      None,
      Some(key),
      Some(scope_ast),
      source_path~,
    )
    build_nested_properties(key_path, glob_index, key, context, source_path)
  }
  let mut target_map = ir
  for i = 0; i < glob_index; i = i + 1 {
    guard target_map.get_field_by_syntax(key_path.path[i]) is Some(field) else {
      return
    }
    match field.composite {
      Some(Map(next)) => target_map = next
      _ => return
    }
  }
  let targets = target_map.glob_targets(pattern)
  // Keep this ordering aligned with `compile_glob_key`: applying a parent
  // clones its composite map, so descendants must be updated first.
  let mut target_index = targets.length() - 1
  while target_index >= 0 {
    let target = targets[target_index]
    let field = target.field
    let parent_map = target.parent_map
    let live_field = parent_map.get_field(field.name).unwrap_or(field)
    let filter_context = FilterContext::new(live_field, parent_map)
    let mut passes = true
    for filter in filters {
      let (negated, filter_key, filter_value) = filter
      let matched = evaluate_filter_at_level(
        filter_context,
        filter_key,
        filter_value,
        target.level,
      )
      if (if negated { matched } else { !matched }) {
        passes = false
        break
      }
    }
    if passes {
      match suspension {
        Some(_) =>
          if glob_operation_follows_field(live_field, key, source_path) {
            let syntax = match live_field.name_syntax() {
              Some(syntax) => syntax
              None => synthetic_unquoted_string(live_field.name)
            }
            let context = RefContext::new(
              None,
              Some(key),
              Some(scope_ast),
              source_path~,
            )
            let _ = record_field_reference(
              parent_map,
              live_field,
              syntax,
              key_path,
              context,
              glob_index,
              true,
              due_to_glob=true,
            )
          }
        None => {
          let updated = apply_properties_to_field(live_field, properties)
          parent_map.set_field(updated)
          apply_nested_globs_with_imports(
            updated,
            key.value,
            resolver,
            import_stack,
            source_path,
          )
        }
      }
    }
    target_index = target_index - 1
  }
}

///|
fn apply_nested_globs_with_imports(
  field : Field,
  value : @ast.Value?,
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path : String?,
) -> Unit raise CompileError {
  guard value is Some(Map(ast_map)) else { return }
  guard field.composite is Some(Map(field_map)) else { return }
  for node in ast_map.nodes {
    match node {
      Key(nested_key) =>
        if !nested_key.ampersand &&
          !nested_key.not_ampersand &&
          key_uses_glob(nested_key) {
          compile_key_with_imports(
            field_map,
            nested_key,
            ast_map,
            [],
            resolver,
            import_stack,
            source_path,
          )
        }
      _ => ()
    }
  }
}

///|
fn compile_value_with_imports(
  field : Field,
  value : @ast.Value,
  parent_map : Map,
  scope_frames : Array[CompileScopeFrame],
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path : String?,
) -> Field raise CompileError {
  match value {
    Scalar(s) =>
      Field::new(
        field.name,
        Some(compile_scalar(s, source_path)),
        field.composite,
        field.references,
        name_syntax=field.name_syntax(),
      )
    Map(m) => {
      let ir_map = match field.composite {
        Some(Map(existing)) => existing
        _ => Map::from_ast(m, source_path~)
      }
      let nested_frames = scope_frames.copy()
      let child_syntax = match field.name_syntax() {
        Some(syntax) => syntax
        None => synthetic_unquoted_string(field.name)
      }
      nested_frames.push(CompileScopeFrame::new(parent_map, child_syntax))
      compile_map_nodes_with_imports(
        ir_map,
        m,
        m.nodes,
        nested_frames,
        resolver,
        import_stack,
        source_path~,
      )
      Field::new(
        field.name,
        field.primary,
        Some(Map(ir_map)),
        field.references,
        name_syntax=field.name_syntax(),
      )
    }
    Array(a) => {
      let ir_array = compile_array_with_imports(
        a, resolver, import_stack, source_path,
      )
      Field::new(
        field.name,
        field.primary,
        Some(Array(ir_array)),
        field.references,
        name_syntax=field.name_syntax(),
      )
    }
    Import(imp) => {
      let (import_ir, selector) = compile_import_target(
        imp, resolver, import_stack,
      )
      let (primary, composite) = select_primary_and_composite(
        import_ir, selector,
      )
      Field::new(
        field.name,
        primary,
        composite,
        field.references,
        name_syntax=field.name_syntax(),
      )
    }
    BlockScalar(label, m) => {
      let field = match label {
        Scalar(s) =>
          Field::new(
            field.name,
            Some(compile_scalar(s, source_path)),
            field.composite,
            field.references,
            name_syntax=field.name_syntax(),
          )
        _ => field
      }
      let ir_map = match field.composite {
        Some(Map(existing)) => existing
        _ => Map::from_ast(m, source_path~)
      }
      let nested_frames = scope_frames.copy()
      let child_syntax = match field.name_syntax() {
        Some(syntax) => syntax
        None => synthetic_unquoted_string(field.name)
      }
      nested_frames.push(CompileScopeFrame::new(parent_map, child_syntax))
      compile_map_nodes_with_imports(
        ir_map,
        m,
        m.nodes,
        nested_frames,
        resolver,
        import_stack,
        source_path~,
      )
      Field::new(
        field.name,
        field.primary,
        Some(Map(ir_map)),
        field.references,
        name_syntax=field.name_syntax(),
      )
    }
  }
}

///|
fn compile_array_with_imports(
  arr : @ast.ArrayValue,
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path : String?,
) -> IRArray raise CompileError {
  let values : Array[Value] = []
  for node in arr.nodes {
    match node {
      Comment(_, _) => ()
      BlockComment(_, _) => ()
      Substitution(sub) =>
        values.push(Scalar(substitution_scalar(sub, source_path)))
      Import(imp) => {
        let (import_ir, selector) = compile_import_target(
          imp, resolver, import_stack,
        )
        if imp.spread {
          // Only allow spreading arrays into arrays (reference behavior).
          let f = select_field(import_ir, selector)
          match f.composite {
            Some(Array(a)) =>
              for v in a.values {
                values.push(v)
              }
            _ => raise ImportError("can only spread import array into array")
          }
        } else {
          let (primary, composite) = select_primary_and_composite(
            import_ir, selector,
          )
          match (primary, composite) {
            (Some(p), _) => values.push(Scalar(p))
            (None, Some(Map(m))) => values.push(Map(m))
            (None, Some(Array(a))) => values.push(Array(a))
            (None, None) => ()
          }
        }
      }
      Value(v) => {
        let compiled = compile_ast_value_with_imports(
          v, resolver, import_stack, source_path,
        )
        values.push(compiled)
      }
    }
  }
  IRArray::from_ast(arr, values, source_path~)
}

///|
fn compile_ast_value_with_imports(
  value : @ast.Value,
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path : String?,
) -> Value raise CompileError {
  match value {
    Scalar(s) => Scalar(compile_scalar(s, source_path))
    Map(m) => {
      let ir_map = Map::from_ast(m, source_path~)
      compile_map_nodes_with_imports(
        ir_map,
        m,
        m.nodes,
        [],
        resolver,
        import_stack,
        source_path~,
      )
      Map(ir_map)
    }
    Array(a) =>
      Array(compile_array_with_imports(a, resolver, import_stack, source_path))
    Import(imp) => {
      let (import_ir, selector) = compile_import_target(
        imp, resolver, import_stack,
      )
      let (primary, composite) = select_primary_and_composite(
        import_ir, selector,
      )
      match (primary, composite) {
        (Some(p), _) => Scalar(p)
        (None, Some(Map(m))) => Map(m)
        (None, Some(Array(a))) => Array(a)
        (None, None) => Scalar(Scalar::null())
      }
    }
    BlockScalar(_, m) => {
      let ir_map = Map::from_ast(m, source_path~)
      compile_map_nodes_with_imports(
        ir_map,
        m,
        m.nodes,
        [],
        resolver,
        import_stack,
        source_path~,
      )
      Map(ir_map)
    }
  }
}

///|
fn compile_edges_with_imports(
  scope_ir : Map,
  key : @ast.Key,
  scope_ast : @ast.Map,
  scope_frames : Array[CompileScopeFrame],
  resolver : ImportResolver,
  import_stack : Array[String],
  source_path : String?,
) -> Unit raise CompileError {
  for edge in key.edges {
    let src_parent_count = leading_parent_count(edge.src)
    let dst_parent_count = leading_parent_count(edge.dst)
    let climb = if src_parent_count > dst_parent_count {
      src_parent_count
    } else {
      dst_parent_count
    }
    let ir = parent_edge_scope(scope_ir, scope_frames, climb)
    let src = edge_path_after_parent_resolution(edge.src, scope_frames, climb)
    let dst = edge_path_after_parent_resolution(edge.dst, scope_frames, climb)
    let edge_context = RefContext::new(
      Some(edge),
      Some(key),
      Some(scope_ast),
      source_path~,
    )
    let src_paths = edge_endpoint_paths(ir, src)
    let dst_paths = edge_endpoint_paths(ir, dst)
    let filters = collect_filters_from_value(key)
    let src_has_glob = edge.src.has_glob()
    let dst_has_glob = edge.dst.has_glob()
    if src_paths.is_empty() || dst_paths.is_empty() {
      continue
    }
    fn next_index_for_group(ir : Map, base_id : EdgeID) -> Int {
      let existing = ir.find_edges(base_id)
      let mut max_idx = -1
      for e in existing {
        match e.id.index {
          Some(i) => if i > max_idx { max_idx = i }
          None => if max_idx < 0 { max_idx = 0 }
        }
      }
      max_idx + 1
    }

    fn find_exact_edge(ir : Map, id : EdgeID) -> Edge? {
      for e in ir.edges {
        if e.id.matches(id) {
          return Some(e)
        }
      }
      None
    }

    fn compile_edge_value(
      existing : Edge?,
      value : @ast.Value?,
      resolver : ImportResolver,
      import_stack : Array[String],
    ) -> (Scalar?, Map?) raise CompileError {
      if key_suspension(key) is Some(_) {
        return match existing {
          Some(edge) => (edge.primary, edge.map)
          None => (None, None)
        }
      }
      let (patch_primary, patch_map) = match value {
        Some(Scalar(s)) => (Some(compile_scalar(s, source_path)), None)
        Some(Map(m)) => {
          let ir_map = Map::from_ast(m, source_path~)
          compile_map_nodes_with_imports(
            ir_map,
            m,
            m.nodes,
            [],
            resolver,
            import_stack,
            source_path~,
          )
          (None, Some(ir_map))
        }
        Some(BlockScalar(label, m)) => {
          let primary = match label {
            Scalar(s) => Some(compile_scalar(s, source_path))
            _ => None
          }
          let ir_map = Map::from_ast(m, source_path~)
          compile_map_nodes_with_imports(
            ir_map,
            m,
            m.nodes,
            [],
            resolver,
            import_stack,
            source_path~,
          )
          (primary, Some(ir_map))
        }
        _ => (None, None)
      }
      let index_glob = match key.edge_index {
        Some(index) => index.glob
        None => false
      }
      let is_glob_update = src_has_glob || dst_has_glob || index_glob
      let patch_overrides = !is_glob_update ||
        glob_edge_patch_follows_existing(existing, key.range.start.offset)
      apply_edge_value(
        existing,
        key.edge_key,
        patch_primary,
        patch_map,
        patch_overrides~,
      )
    }

    if path_is_recursive_glob(src) &&
      path_is_recursive_glob(dst) &&
      key.edge_index.map(fn(index) { index.glob }).unwrap_or(false) {
      let targets : Array[RecursiveEdgeTarget] = []
      let include_boards = match (src.path[0], dst.path[0]) {
        (Unquoted(a), Unquoted(b)) =>
          is_triple_glob(a.pattern) || is_triple_glob(b.pattern)
        _ => false
      }
      collect_recursive_edge_targets(
        ir,
        ResolvedPath::new([], []),
        include_boards,
        targets,
      )
      for target in targets {
        if target.edge.id.src_arrow != (edge.src_arrow == "<") ||
          target.edge.id.dst_arrow != (edge.dst_arrow == ">") {
          continue
        }
        if key_suspension(key) is Some(_) &&
          !glob_edge_patch_follows_existing(
            Some(target.edge),
            key.range.start.offset,
          ) {
          continue
        }
        if !edge_glob_filters_pass(
            ir,
            Some(target.edge),
            target.src_path,
            target.dst_path,
            filters,
          ) {
          continue
        }
        let references = target.edge.references.copy()
        references.push(
          EdgeReference::from_ast(
            edge,
            target.edge.id.index.unwrap_or(0),
            true,
            edge_context,
            due_to_glob=true,
          ),
        )
        let (primary, edge_map) = compile_edge_value(
          Some(target.edge),
          key.value,
          resolver,
          import_stack,
        )
        target.parent.set_edge(
          Edge::new(target.edge.id, primary, edge_map, references),
        )
      }
      continue
    }

    for src_path in src_paths {
      for dst_path in dst_paths {
        if !edge_glob_filters_pass(ir, None, src_path, dst_path, filters) {
          continue
        }
        if (src_has_glob || dst_has_glob) &&
          string_path_equal(src_path, dst_path) {
          continue
        }
        let base_id = EdgeID::new(
          src_path.names,
          dst_path.names,
          edge.src_arrow == "<",
          edge.dst_arrow == ">",
          src_path_syntax=Some(src_path.syntax),
          dst_path_syntax=Some(dst_path.syntax),
        )
        match key.edge_index {
          Some(ei) =>
            if ei.glob {
              let existing = ir.find_edges(base_id)
              for e in existing {
                if key_suspension(key) is Some(_) &&
                  !glob_edge_patch_follows_existing(
                    Some(e),
                    key.range.start.offset,
                  ) {
                  continue
                }
                if !edge_glob_filters_pass(
                    ir,
                    Some(e),
                    src_path,
                    dst_path,
                    filters,
                  ) {
                  continue
                }
                e.references.push(
                  EdgeReference::from_ast(
                    edge,
                    e.id.index.unwrap_or(0),
                    true,
                    edge_context,
                    due_to_glob=true,
                  ),
                )
                ensure_field_path(ir, src_path)
                ensure_field_path(ir, dst_path)
                append_field_references_for_path(
                  ir, src_path, edge_context, true,
                )
                append_field_references_for_path(
                  ir, dst_path, edge_context, true,
                )
                let (primary, edge_map) = compile_edge_value(
                  Some(e),
                  key.value,
                  resolver,
                  import_stack,
                )
                ir.set_edge(Edge::new(e.id, primary, edge_map, e.references))
              }
            } else {
              let idx = ei.index.unwrap_or(0)
              let edge_id = EdgeID::with_index(
                base_id.src_path,
                base_id.dst_path,
                base_id.src_arrow,
                base_id.dst_arrow,
                idx,
                src_path_syntax=Some(base_id.src_path_syntax),
                dst_path_syntax=Some(base_id.dst_path_syntax),
              )
              match find_exact_edge(ir, edge_id) {
                Some(e) => {
                  e.references.push(
                    EdgeReference::from_ast(
                      edge,
                      idx,
                      true,
                      edge_context,
                      due_to_glob=src_has_glob || dst_has_glob,
                    ),
                  )
                  ensure_field_path(ir, src_path)
                  ensure_field_path(ir, dst_path)
                  append_field_references_for_path(
                    ir,
                    src_path,
                    edge_context,
                    src_has_glob || dst_has_glob,
                  )
                  append_field_references_for_path(
                    ir,
                    dst_path,
                    edge_context,
                    src_has_glob || dst_has_glob,
                  )
                  let (primary, edge_map) = compile_edge_value(
                    Some(e),
                    key.value,
                    resolver,
                    import_stack,
                  )
                  ir.set_edge(Edge::new(e.id, primary, edge_map, e.references))
                }
                None => {
                  let (primary, edge_map) = compile_edge_value(
                    None,
                    key.value,
                    resolver,
                    import_stack,
                  )
                  let new_edge = Edge::new(edge_id, primary, edge_map, [
                    EdgeReference::from_ast(
                      edge,
                      idx,
                      true,
                      edge_context,
                      due_to_glob=src_has_glob || dst_has_glob,
                    ),
                  ])
                  ir.add_edge(new_edge)
                  ensure_field_path(ir, src_path)
                  ensure_field_path(ir, dst_path)
                  append_field_references_for_path(
                    ir,
                    src_path,
                    edge_context,
                    src_has_glob || dst_has_glob,
                  )
                  append_field_references_for_path(
                    ir,
                    dst_path,
                    edge_context,
                    src_has_glob || dst_has_glob,
                  )
                }
              }
            }
          None => {
            let idx = next_index_for_group(ir, base_id)
            let edge_id = EdgeID::with_index(
              base_id.src_path,
              base_id.dst_path,
              base_id.src_arrow,
              base_id.dst_arrow,
              idx,
              src_path_syntax=Some(base_id.src_path_syntax),
              dst_path_syntax=Some(base_id.dst_path_syntax),
            )
            let (primary, edge_map) = compile_edge_value(
              None,
              key.value,
              resolver,
              import_stack,
            )
            let new_edge = Edge::new(edge_id, primary, edge_map, [
              EdgeReference::from_ast(
                edge,
                idx,
                true,
                edge_context,
                due_to_glob=src_has_glob || dst_has_glob,
              ),
            ])
            ir.add_edge(new_edge)
            ensure_field_path(ir, src_path)
            ensure_field_path(ir, dst_path)
            append_field_references_for_path(
              ir,
              src_path,
              edge_context,
              src_has_glob || dst_has_glob,
            )
            append_field_references_for_path(
              ir,
              dst_path,
              edge_context,
              src_has_glob || dst_has_glob,
            )
          }
        }
      }
    }
  }
}