# Knowledge Base Configuration Composition Helpers
#
# Provides utilities for merging configurations from multiple layers:
# 1. Schema (type contracts)
# 2. Defaults (base values)
# 3. Mode Overlay (mode-specific tuning: dev/prod/test)
# 4. User Customization (overrides)
#
# Pattern: Follows provisioning/schemas/platform/common/helpers.ncl

{
  # Recursively merge two record configurations
  # Override values take precedence over base (shallow merge at each level)
  #
  # Example:
  #   let base = { storage = { primary = 'filesystem }, embeddings = { enabled = true } }
  #   let override = { storage = { primary = 'surrealdb } }
  #   merge_with_override base override
  #   # Result: { storage = { primary = 'surrealdb }, embeddings = { enabled = true } }
  merge_with_override | not_exported = fun base override =>
    if std.is_record base && std.is_record override then
      let base_fields = std.record.fields base in
      let override_fields = std.record.fields override in

      base_fields
      |> std.array.fold_right
        (fun key acc =>
          let base_value = base."%{key}" in

          if std.record.has_field key override then
            let override_value = override."%{key}" in

            if std.is_record base_value && std.is_record override_value then
              acc
              & { "%{key}" = merge_with_override base_value override_value }
            else
              # Override value takes precedence
              acc & { "%{key}" = override_value }
          else
            # Keep base value
            acc & { "%{key}" = base_value }
        )
        (override_fields
         |> std.array.fold_right
           (fun key acc =>
             if !std.record.has_field key base then
               acc & { "%{key}" = override."%{key}" }
             else
               acc
           )
           {}
        )
    else
      # If either is not a record, override takes precedence
      if std.is_null override then base else override,

  # Compose configuration from multiple layers with proper merging
  #
  # Layer 1: defaults (base values)
  # Layer 2: mode_config (mode-specific overrides: dev/prod/test)
  # Layer 3: user_custom (user customizations)
  #
  # Example:
  #   let defaults = { embeddings = { provider = 'fastembed } }
  #   let mode = { embeddings = { provider = 'openai } }  # Production override
  #   let user = { graph = { name = "my-project" } }
  #   compose_config defaults mode user
  compose_config | not_exported = fun defaults mode_config user_custom =>
    let with_mode = merge_with_override defaults mode_config in
    merge_with_override with_mode user_custom,

  # Compose minimal config (defaults + user only, no mode)
  # Useful for simple cases where mode-specific tuning isn't needed
  compose_minimal | not_exported = fun defaults user_custom =>
    merge_with_override defaults user_custom,

  # Validate that required fields are present
  # Returns config if valid, throws error if invalid
  validate_required | not_exported = fun config required_fields =>
    required_fields
    |> std.array.fold_right
      (fun field acc =>
        if std.record.has_field field config then
          acc
        else
          std.fail "Required field missing: %{field}"
      )
      config,

  # Extract specific subsection from config
  # Example: extract_section config "storage"
  extract_section | not_exported = fun config section =>
    if std.record.has_field section config then
      config."%{section}"
    else
      std.fail "Section not found: %{section}",

  # Merge arrays (for things like watch_paths, tags, etc.)
  # Deduplicates and preserves order
  merge_arrays | not_exported = fun base override =>
    let combined = base @ override in
    std.array.fold_right
      (fun item acc =>
        if std.array.elem item acc then
          acc
        else
          [item] @ acc
      )
      []
      combined,
}
