# File operations for interactive setup.

# Registry of temp files created during this session; cleaned up on exit.
_FILE_OPS_CLEANUP_TMP=()
declare -A _FILE_OPS_VOLUME_BLOCKS=()
declare -a _FILE_OPS_VOLUME_ORDER=()
_WIZARD_MANAGED_SERVICES_MARKER="# __WIZARD_MANAGED_SERVICES__"
_file_ops_cleanup() {
  local f
  for f in "${_FILE_OPS_CLEANUP_TMP[@]:-}"; do
    rm -f "$f" 2>/dev/null || true
  done
}
trap '_file_ops_cleanup' EXIT INT TERM

format_env_value() {
  local value="$1"
  local escaped

  if [[ -z "$value" ]]; then
    printf ''
    return
  fi

  if [[ "$value" =~ [[:space:]] || "$value" == *"\""* || "$value" == *"$"* || "$value" == *"#"* ]]; then
    # Prefer single quotes when quoting is required so generated .env values
    # match env.example style and remain Compose-friendly. Fall back to
    # double quotes only when the value itself contains a single quote.
    if [[ "$value" != *"'"* ]]; then
      printf "'%s'" "$value"
      return
    fi

    # Double-quoted .env values only need escaping for backslash and double quote.
    # Do not escape '$': python-dotenv preserves plain '$' literally, while '\$'
    # changes the loaded value.
    # '#' in unquoted values is treated as a comment by python-dotenv, so any
    # value containing '#' must be quoted.
    escaped="${value//\\/\\\\}"
    escaped="${escaped//\"/\\\"}"
    printf '"%s"' "$escaped"
    return
  fi

  printf '%s' "$value"
}

backup_env_file() {
  local env_file="${1:-${REPO_ROOT:-.}/.env}"
  local backup_file=""

  if [[ -f "$env_file" ]]; then
    backup_file="${env_file}.backup.$(date +%Y%m%d_%H%M%S)"
    if ! cp "$env_file" "$backup_file"; then
      format_error "Failed to back up ${env_file} to ${backup_file}." "Check disk space and file permissions."
      return 1
    fi
    printf '%s' "$backup_file"
  fi
}

backup_compose_file() {
  local compose_file="${1:-}"
  local repo_root="${REPO_ROOT:-.}"
  local backup_file=""

  if [[ -z "$compose_file" ]]; then
    compose_file="$(find_generated_compose_file)"
  fi

  if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then
    return 0
  fi

  backup_file="${repo_root}/docker-compose.backup$(date +%Y%m%d_%H%M%S).yml"
  if ! cp "$compose_file" "$backup_file"; then
    format_error "Failed to back up ${compose_file} to ${backup_file}." \
      "Check disk space and file permissions."
    return 1
  fi

  printf '%s' "$backup_file"
}

stage_ssl_assets() {
  local cert_source="$1"
  local key_source="$2"
  local certs_dir="${REPO_ROOT:-.}/data/certs"
  local cert_target=""
  local key_target=""

  mkdir -p "$certs_dir"

  if [[ -n "$cert_source" ]]; then
    cert_target="${certs_dir}/$(resolve_staged_ssl_basename "cert" "$cert_source" "$key_source")"
    if [[ ! -e "$cert_target" || ! "$cert_source" -ef "$cert_target" ]]; then
      cp "$cert_source" "$cert_target"
    fi
  fi

  if [[ -n "$key_source" ]]; then
    key_target="${certs_dir}/$(resolve_staged_ssl_basename "key" "$key_source" "$cert_source")"
    if [[ ! -e "$key_target" || ! "$key_source" -ef "$key_target" ]]; then
      cp "$key_source" "$key_target"
    fi
  fi
}

stage_redis_config_asset() {
  local template_path="${TEMPLATES_DIR:-}/redis.conf.template"
  local config_dir="${REPO_ROOT:-.}/data/config"
  local config_target="${config_dir}/redis.conf"

  if [[ -z "$template_path" || ! -f "$template_path" ]]; then
    format_error "Missing Redis config template: ${template_path}" \
      "Restore scripts/setup/templates/redis.conf.template before rerunning setup."
    return 1
  fi

  mkdir -p "$config_dir"

  if [[ -e "$config_target" ]]; then
    log_info "Preserving existing Redis config at ${config_target}"
    return 0
  fi

  if ! cp "$template_path" "$config_target"; then
    format_error "Failed to stage Redis config at ${config_target}" \
      "Check file permissions and available disk space, then rerun setup."
    return 1
  fi

  log_success "Staged Redis config at ${config_target}"
}

resolve_staged_ssl_basename() {
  local asset_type="$1"
  local source_path="$2"
  local peer_path="${3:-}"
  local basename_value=""
  local peer_basename=""

  basename_value="$(basename "$source_path")"
  if [[ -n "$peer_path" ]]; then
    peer_basename="$(basename "$peer_path")"
    if [[ "$basename_value" == "$peer_basename" ]]; then
      printf '%s-%s' "$asset_type" "$basename_value"
      return 0
    fi
  fi

  printf '%s' "$basename_value"
}

append_preserved_non_template_env_lines() {
  local template_file="$1"
  local existing_env_file="$2"
  local output_file="$3"
  local line key
  local in_preserved_section="no"
  local line_is_commented_env="no"
  local template_in_preserved_section="no"
  local template_has_preserved_section="no"
  local old_has_preserved_section="no"
  local preserved_header="### ----- Preserved custom environment variables from previous .env  -----"
  local preserved_notice="### ----- Comments in this session will persist across regenerations -----"
  local -a pending_lines=()
  local -a preserved_payload=()
  local -a discovered_payload=()
  local -a template_preserved_payload=()
  local -a effective_preserved_payload=()
  local -A ignored_keys=(
    ["LIGHTRAG_SETUP_PROFILE"]=1
  )
  local -A template_keys=()

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "$preserved_header" ]]; then
      template_has_preserved_section="yes"
      template_in_preserved_section="yes"
      continue
    fi

    if [[ "$template_in_preserved_section" == "yes" && "$line" == "$preserved_notice" ]]; then
      continue
    fi

    if [[ "$template_in_preserved_section" == "yes" ]]; then
      template_preserved_payload+=("$line")
    fi

    if [[ "$line" =~ ^[A-Za-z0-9_]+= ]]; then
      template_keys["${line%%=*}"]=1
    elif [[ "$line" =~ ^#[[:space:]]*([A-Za-z0-9_]+)=(.*)$ ]]; then
      template_keys["${BASH_REMATCH[1]}"]=1
    fi
  done < "$template_file"

  if [[ -f "$existing_env_file" ]]; then
    while IFS= read -r line || [[ -n "$line" ]]; do
      if [[ "$line" == "$preserved_header" ]]; then
        in_preserved_section="yes"
        old_has_preserved_section="yes"
        pending_lines=()
        continue
      fi

      if [[ "$in_preserved_section" == "yes" && "$line" == "$preserved_notice" ]]; then
        continue
      fi

      key=""
      line_is_commented_env="no"

      if [[ "$line" =~ ^([A-Za-z0-9_]+)= ]]; then
        key="${BASH_REMATCH[1]}"
      elif [[ "$line" =~ ^#[[:space:]]*([A-Za-z0-9_]+)=(.*)$ ]]; then
        key="${BASH_REMATCH[1]}"
        line_is_commented_env="yes"
      fi

      if [[ -z "$key" ]]; then
        if [[ "$in_preserved_section" == "yes" ]]; then
          pending_lines+=("$line")
        fi
        continue
      fi

      if [[ -n "${ignored_keys[$key]+set}" ]]; then
        if [[ "$in_preserved_section" == "yes" ]] && ((${#pending_lines[@]} > 0)); then
          preserved_payload+=("${pending_lines[@]}")
        fi
        pending_lines=()
        continue
      fi

      if [[ -z "${template_keys[$key]+set}" || \
        ("$in_preserved_section" == "yes" && "$line_is_commented_env" == "yes") ]]; then
        if ((${#pending_lines[@]} > 0)); then
          if [[ "$in_preserved_section" == "yes" ]]; then
            preserved_payload+=("${pending_lines[@]}")
          fi
        fi

        if [[ "$in_preserved_section" == "yes" ]]; then
          preserved_payload+=("$line")
        else
          discovered_payload+=("$line")
        fi
      elif [[ "$in_preserved_section" == "yes" && ${#pending_lines[@]} -gt 0 ]]; then
        preserved_payload+=("${pending_lines[@]}")
      fi

      pending_lines=()
    done < "$existing_env_file"

    if ((${#pending_lines[@]} > 0)); then
      preserved_payload+=("${pending_lines[@]}")
    fi
  fi

  effective_preserved_payload=("${preserved_payload[@]}")

  if [[ "$template_has_preserved_section" == "yes" ]]; then
    printf '%s\n%s\n' "$preserved_header" "$preserved_notice" >> "$output_file"

    if [[ "$old_has_preserved_section" != "yes" ]] && ((${#template_preserved_payload[@]} > 0)); then
      printf '%s\n' "${template_preserved_payload[@]}" >> "$output_file"
    fi

    if ((${#effective_preserved_payload[@]} > 0)); then
      printf '%s\n' "${effective_preserved_payload[@]}" >> "$output_file"
    fi

    if ((${#discovered_payload[@]} > 0)); then
      printf '%s\n' "${discovered_payload[@]}" >> "$output_file"
    fi
    return 0
  fi

  if ((${#effective_preserved_payload[@]} == 0 && ${#discovered_payload[@]} == 0)); then
    return 0
  fi

  printf '\n%s\n%s\n' "$preserved_header" "$preserved_notice" >> "$output_file"

  if ((${#effective_preserved_payload[@]} > 0)); then
    printf '%s\n' "${effective_preserved_payload[@]}" >> "$output_file"
  fi

  if ((${#discovered_payload[@]} > 0)); then
    printf '%s\n' "${discovered_payload[@]}" >> "$output_file"
  fi
}

generate_env_file() {
  local template_file="${1:-${REPO_ROOT:-.}/env.example}"
  local output_file="${2:-${REPO_ROOT:-.}/.env}"
  local tmp_file="${output_file}.tmp"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line key value
  local preserved_header="### ----- Preserved custom environment variables from previous .env  -----"
  local in_template_preserved_section="no"
  local -A written_keys=()
  local -A match_write_keys=()

  if [[ ! -f "$template_file" ]]; then
    echo "env.example not found at $template_file" >&2
    return 1
  fi

  # Pre-scan: identify commented keys whose value exactly matches the ENV_VALUE.
  # When a match exists, the active value is written only at that matching line,
  # leaving all other commented examples intact.
  local _prescan_key _prescan_val _prescan_env_val _prescan_fmt
  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "$preserved_header" ]]; then
      in_template_preserved_section="yes"
      continue
    fi

    if [[ "$in_template_preserved_section" == "yes" ]]; then
      continue
    fi

    if [[ "$line" =~ ^#[[:space:]]*([A-Za-z0-9_]+)=(.*)$ ]]; then
      _prescan_key="${BASH_REMATCH[1]}"
      _prescan_val="${BASH_REMATCH[2]}"
      if [[ -z "${match_write_keys[$_prescan_key]+set}" && -n "${ENV_VALUES[$_prescan_key]+set}" ]]; then
        _prescan_env_val="${ENV_VALUES[$_prescan_key]}"
        _prescan_fmt="$(format_env_value "$_prescan_env_val" "$_prescan_key")"
        if [[ "$_prescan_val" == "$_prescan_env_val" || "$_prescan_val" == "$_prescan_fmt" ]]; then
          match_write_keys["$_prescan_key"]=1
        fi
      fi
    fi
  done < "$template_file"

  : > "$tmp_file"

  in_template_preserved_section="no"
  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "$preserved_header" ]]; then
      in_template_preserved_section="yes"
      continue
    fi

    if [[ "$in_template_preserved_section" == "yes" ]]; then
      continue
    fi

    if [[ "$line" =~ ^[A-Za-z0-9_]+= ]]; then
      key="${line%%=*}"
      if [[ -z "${written_keys[$key]+set}" ]]; then
        if [[ -n "${ENV_VALUES[$key]+set}" ]]; then
          value="${ENV_VALUES[$key]}"
          local _fmt_active_val
          _fmt_active_val="$(format_env_value "$value" "$key")"
          printf '%s=%s\n' "$key" "$_fmt_active_val" >> "$tmp_file"
          local _orig_tmpl_val="${line#*=}"
          if [[ "$_orig_tmpl_val" != "$value" && "$_orig_tmpl_val" != "$_fmt_active_val" ]]; then
            printf '# %s\n' "$line" >> "$tmp_file"
          fi
        else
          printf '%s\n' "$line" >> "$tmp_file"
        fi
        written_keys["$key"]=1
      else
        if [[ -n "${ENV_VALUES[$key]+set}" ]]; then
          printf '# %s\n' "$line" >> "$tmp_file"
        else
          printf '%s\n' "$line" >> "$tmp_file"
        fi
      fi
    elif [[ "$line" =~ ^#[[:space:]]*([A-Za-z0-9_]+)=(.*)$ ]]; then
      key="${BASH_REMATCH[1]}"
      local _commented_val="${BASH_REMATCH[2]}"
      if [[ -z "${written_keys[$key]+set}" && -n "${ENV_VALUES[$key]+set}" ]]; then
        value="${ENV_VALUES[$key]}"
        if [[ -n "${match_write_keys[$key]+set}" ]]; then
          # A commented line matching the ENV value exists; only activate at that line.
          local _fmt_val
          _fmt_val="$(format_env_value "$value" "$key")"
          if [[ "$_commented_val" == "$value" || "$_commented_val" == "$_fmt_val" ]]; then
            printf '%s=%s\n' "$key" "$_fmt_val" >> "$tmp_file"
            written_keys["$key"]=1
          else
            printf '%s\n' "$line" >> "$tmp_file"
          fi
        else
          # No matching commented line; fall back to activating at first occurrence.
          printf '%s=%s\n' "$key" "$(format_env_value "$value" "$key")" >> "$tmp_file"
          written_keys["$key"]=1
        fi
      else
        printf '%s\n' "$line" >> "$tmp_file"
      fi
    else
      printf '%s\n' "$line" >> "$tmp_file"
    fi
  done < "$template_file"

  append_preserved_non_template_env_lines "$template_file" "$output_file" "$tmp_file"

  mv "$tmp_file" "$output_file"
}

# All environment keys the wizard may inject into the lightrag service via
# COMPOSE_ENV_OVERRIDES.  Used to remove stale entries before re-injection so
# keys no longer needed are not left behind in the compose file.
_WIZARD_COMPOSE_LIGHTRAG_KEYS=(
  "EMBEDDING_BINDING_HOST" "RERANK_BINDING_HOST" "LLM_BINDING_HOST"
  "REDIS_URI" "MONGO_URI" "NEO4J_URI" "MILVUS_URI" "QDRANT_URL" "MEMGRAPH_URI" "OPENSEARCH_HOSTS"
  "POSTGRES_HOST" "POSTGRES_PORT" "PORT" "HOST" "SSL_CERTFILE" "SSL_KEYFILE"
  "WORKING_DIR" "INPUT_DIR" "PROMPT_DIR"
)

_managed_service_root_name() {
  local service_name="$1"

  case "$service_name" in
    postgres|neo4j|mongodb|redis|qdrant|memgraph|vllm-embed|vllm-rerank)
      printf '%s' "$service_name"
      ;;
    opensearch|dashboards)
      printf 'opensearch'
      ;;
    milvus|milvus-etcd|milvus-minio)
      printf 'milvus'
      ;;
    *)
      printf ''
      ;;
  esac
}

_managed_volume_root_name() {
  local volume_name="$1"

  case "$volume_name" in
    postgres_data)
      printf 'postgres'
      ;;
    neo4j_data)
      printf 'neo4j'
      ;;
    mongo_data|mongo_config_data|mongo_mongot_data)
      printf 'mongodb'
      ;;
    redis_data)
      printf 'redis'
      ;;
    milvus_data|milvus-etcd_data|milvus-minio_data)
      printf 'milvus'
      ;;
    qdrant_data)
      printf 'qdrant'
      ;;
    memgraph_data)
      printf 'memgraph'
      ;;
    opensearch_data)
      printf 'opensearch'
      ;;
    vllm_rerank_cache)
      printf 'vllm-rerank'
      ;;
    vllm_embed_cache)
      printf 'vllm-embed'
      ;;
    *)
      printf ''
      ;;
  esac
}

_should_rewrite_wizard_managed_root_service() {
  local root_service="$1"

  if [[ -z "$root_service" ]]; then
    return 1
  fi

  if [[ "${FORCE_REWRITE_COMPOSE:-no}" == "yes" ]]; then
    return 0
  fi

  if [[ -z "${DOCKER_SERVICE_SET[$root_service]+set}" ]]; then
    return 0
  fi

  if [[ -n "${COMPOSE_REWRITE_SERVICE_SET[$root_service]+set}" ]]; then
    return 0
  fi

  return 1
}

_should_preserve_wizard_managed_root_service() {
  local root_service="$1"

  if [[ -z "$root_service" || "${FORCE_REWRITE_COMPOSE:-no}" == "yes" ]]; then
    return 1
  fi

  if [[ -z "${DOCKER_SERVICE_SET[$root_service]+set}" ]]; then
    return 1
  fi

  if [[ -n "${COMPOSE_REWRITE_SERVICE_SET[$root_service]+set}" ]]; then
    return 1
  fi

  return 0
}

_existing_managed_root_service_present() {
  local root_service="$1"

  [[ -n "$root_service" && -n "${EXISTING_MANAGED_ROOT_SERVICE_SET[$root_service]+set}" ]]
}

_refresh_existing_managed_root_service_set_from_compose() {
  local compose_file="$1"
  local service_name

  EXISTING_MANAGED_ROOT_SERVICE_SET=()

  if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then
    return 0
  fi

  while IFS= read -r service_name; do
    EXISTING_MANAGED_ROOT_SERVICE_SET["$service_name"]=1
  done < <(detect_managed_root_services "$compose_file")
}

_is_wizard_managed_root_service_name() {
  local service_name="$1"

  [[ -n "$(_managed_service_root_name "$service_name")" ]]
}

_is_wizard_managed_service_name() {
  local service_name="$1"

  [[ -n "$(_managed_service_root_name "$service_name")" ]]
}

_is_wizard_managed_volume_name() {
  local volume_name="$1"

  [[ -n "$(_managed_volume_root_name "$volume_name")" ]]
}

# Remove wizard-managed keys from the lightrag service's environment block,
# leaving any user-added keys intact.
_strip_lightrag_wizard_environment_keys() {
  local compose_file="$1"
  local tmp_file="${compose_file}.strip-wizard-keys"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line key wk list_entry
  local in_lightrag="no"
  local in_environment="no"

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_environment="no"
    elif [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]]; then
      in_lightrag="no"
      in_environment="no"
    fi

    if [[ "$in_lightrag" == "yes" && "$line" == "    environment:" ]]; then
      in_environment="yes"
      printf '%s\n' "$line" >> "$tmp_file"
      continue
    fi

    if [[ "$in_lightrag" == "yes" && "$in_environment" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{6}([A-Z0-9_]+): ]]; then
        key="${BASH_REMATCH[1]}"
        for wk in "${_WIZARD_COMPOSE_LIGHTRAG_KEYS[@]}"; do
          if [[ "$key" == "$wk" ]]; then
            continue 2  # skip this wizard-managed key
          fi
        done
      elif [[ "$line" =~ ^[[:space:]]{6}-[[:space:]](.+)$ ]]; then
        list_entry="$(_strip_wrapping_quotes "${BASH_REMATCH[1]}")"
        key="${list_entry%%=*}"
        if [[ "$key" =~ ^[A-Z0-9_]+$ ]]; then
          for wk in "${_WIZARD_COMPOSE_LIGHTRAG_KEYS[@]}"; do
            if [[ "$key" == "$wk" ]]; then
              continue 2  # skip this wizard-managed key
            fi
          done
        fi
      elif [[ -z "$line" ]]; then
        continue  # skip blank lines inside the environment block
      elif [[ ! "$line" =~ ^[[:space:]]{6} ]]; then
        in_environment="no"
      fi
    fi

    printf '%s\n' "$line" >> "$tmp_file"
  done < "$compose_file"

  mv "$tmp_file" "$compose_file"
}

_write_service_environment_entries() {
  local tmp_file="$1"
  local style="$2"
  shift 2
  local entries=("$@")
  local key value

  for entry in "${entries[@]}"; do
    key="${entry%%=*}"
    value="${entry#*=}"
    if [[ "$style" == "list" ]]; then
      if [[ "$value" == "${_COMPOSE_RAW_VALUE_PREFIX}"* ]]; then
        printf '      - %s=%s\n' "$key" "${value#${_COMPOSE_RAW_VALUE_PREFIX}}" >> "$tmp_file"
      else
        printf '      - %s\n' "$(format_yaml_value "${key}=${value}")" >> "$tmp_file"
      fi
    else
      printf '      %s: %s\n' "$key" "$(format_compose_environment_value "$value")" >> "$tmp_file"
    fi
  done
}

# Capture top-level named volume blocks so user-managed definitions can be
# re-emitted when still referenced by preserved services.
_collect_top_level_volume_blocks() {
  local compose_file="$1"
  local line
  local in_top_volumes="no"
  local current_volume=""
  local current_block=""

  _FILE_OPS_VOLUME_BLOCKS=()
  _FILE_OPS_VOLUME_ORDER=()

  if [[ ! -f "$compose_file" ]]; then
    return 0
  fi

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" =~ ^[A-Za-z] ]]; then
      if [[ "$in_top_volumes" == "yes" && -n "$current_volume" ]]; then
        _FILE_OPS_VOLUME_BLOCKS["$current_volume"]="$current_block"
        _FILE_OPS_VOLUME_ORDER+=("$current_volume")
      fi

      current_volume=""
      current_block=""
      if [[ "$line" == "volumes:" ]]; then
        in_top_volumes="yes"
      else
        in_top_volumes="no"
      fi
      continue
    fi

    if [[ "$in_top_volumes" != "yes" ]]; then
      continue
    fi

    if [[ "$line" =~ ^[[:space:]]{2}([A-Za-z0-9_.-]+):[[:space:]]*$ ]]; then
      if [[ -n "$current_volume" ]]; then
        _FILE_OPS_VOLUME_BLOCKS["$current_volume"]="$current_block"
        _FILE_OPS_VOLUME_ORDER+=("$current_volume")
      fi

      current_volume="${BASH_REMATCH[1]}"
      current_block="${line}"$'\n'
      continue
    fi

    if [[ -n "$current_volume" ]]; then
      current_block+="${line}"$'\n'
    fi
  done < "$compose_file"

  if [[ "$in_top_volumes" == "yes" && -n "$current_volume" ]]; then
    _FILE_OPS_VOLUME_BLOCKS["$current_volume"]="$current_block"
    _FILE_OPS_VOLUME_ORDER+=("$current_volume")
  fi
}

# Remove wizard-managed services and the top-level volumes block from a compose
# file. Non-managed services are preserved verbatim.
_strip_wizard_managed_services_and_top_level_volumes() {
  local compose_file="$1"
  local tmp_file="${compose_file}.strip-svc"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line current_service="" current_root_service=""
  local in_services="no"
  local in_top_volumes="no"
  local inserted_marker="no"

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "$_WIZARD_MANAGED_SERVICES_MARKER" ]]; then
      continue
    fi

    # Detect top-level (non-indented) keys.
    if [[ "$line" =~ ^[A-Za-z] ]]; then
      if [[ "$in_services" == "yes" && "$line" != "services:" && "$inserted_marker" != "yes" ]]; then
        printf '%s\n' "$_WIZARD_MANAGED_SERVICES_MARKER" >> "$tmp_file"
        inserted_marker="yes"
      fi
      in_top_volumes="no"
      if [[ "$line" == "services:" ]]; then
        in_services="yes"
        current_service=""
      elif [[ "$line" =~ ^volumes:[[:space:]]*$ ]]; then
        in_top_volumes="yes"
        in_services="no"
        current_service=""
        continue  # skip volumes: header; regenerated at end of generate_docker_compose
      else
        in_services="no"
        current_service=""
      fi
      printf '%s\n' "$line" >> "$tmp_file"
      continue
    fi

    # Skip top-level volumes block content.
    if [[ "$in_top_volumes" == "yes" ]]; then
      continue
    fi

    # Track current service inside the services: block.
    if [[ "$in_services" == "yes" && "$line" =~ ^[[:space:]]{2}([A-Za-z0-9_-]+):[[:space:]]*$ ]]; then
      current_service="${BASH_REMATCH[1]}"
      current_root_service="$(_managed_service_root_name "$current_service")"
    fi

    # Skip managed services that are being removed or regenerated. Preserve
    # lightrag, user-added services, and unchanged managed service groups.
    if [[ "$in_services" == "yes" && -n "$current_service" ]] && \
      [[ "$current_service" != "lightrag" ]] && \
      [[ -n "$current_root_service" ]] && \
      _should_rewrite_wizard_managed_root_service "$current_root_service"; then
      continue
    fi

    printf '%s\n' "$line" >> "$tmp_file"
  done < "$compose_file"

  if [[ "$in_services" == "yes" && "$inserted_marker" != "yes" ]]; then
    printf '%s\n' "$_WIZARD_MANAGED_SERVICES_MARKER" >> "$tmp_file"
  fi

  mv "$tmp_file" "$compose_file"
}

_merge_managed_service_blocks() {
  local compose_file="$1"
  local service_blocks_file="$2"
  local tmp_file="${compose_file}.merge-services"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local inserted="no"

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "$_WIZARD_MANAGED_SERVICES_MARKER" ]]; then
      if [[ -s "$service_blocks_file" ]]; then
        cat "$service_blocks_file" >> "$tmp_file"
        inserted="yes"
      fi
      continue
    fi

    printf '%s\n' "$line" >> "$tmp_file"
  done < "$compose_file"

  if [[ -s "$service_blocks_file" && "$inserted" != "yes" ]]; then
    cat "$service_blocks_file" >> "$tmp_file"
  fi

  mv "$tmp_file" "$compose_file"
}

_normalize_services_section_spacing() {
  local compose_file="$1"
  local tmp_file="${compose_file}.normalize-services"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_services="no"
  local pending_blank="no"
  local saw_service_content="no"

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_services" == "yes" ]]; then
      if [[ "$line" == "$_WIZARD_MANAGED_SERVICES_MARKER" ]]; then
        continue
      fi

      if [[ -z "$line" ]]; then
        pending_blank="yes"
        continue
      fi

      if [[ ! "$line" =~ ^[[:space:]] ]]; then
        pending_blank="no"
        if [[ "$saw_service_content" == "yes" ]]; then
          printf '\n' >> "$tmp_file"
        fi
        printf '%s\n' "$line" >> "$tmp_file"
        in_services="no"
        continue
      fi

      if [[ "$pending_blank" == "yes" && "$saw_service_content" == "yes" ]]; then
        printf '\n' >> "$tmp_file"
      fi

      printf '%s\n' "$line" >> "$tmp_file"
      pending_blank="no"
      saw_service_content="yes"
      continue
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "services:" ]]; then
      in_services="yes"
      pending_blank="no"
      saw_service_content="no"
    fi
  done < "$compose_file"

  mv "$tmp_file" "$compose_file"
}

_strip_wrapping_quotes() {
  local value="$1"

  if [[ "$value" == \"*\" && "$value" == *\" ]]; then
    value="${value#\"}"
    value="${value%\"}"
  elif [[ "$value" == \'*\' ]]; then
    value="${value#\'}"
    value="${value%\'}"
  fi

  printf '%s' "$value"
}

read_service_environment_value() {
  local compose_file="$1"
  local service_name="$2"
  local wanted_key="$3"
  local line
  local entry_key=""
  local entry_value=""
  local service_header="  ${service_name}:"
  local in_service="no"
  local in_environment="no"

  if [[ ! -f "$compose_file" ]]; then
    return 1
  fi

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_service" == "yes" && "$in_environment" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{6}([A-Z0-9_]+):[[:space:]]*(.+)$ ]]; then
        entry_key="${BASH_REMATCH[1]}"
        if [[ "$entry_key" == "$wanted_key" ]]; then
          printf '%s' "$(_strip_wrapping_quotes "${BASH_REMATCH[2]}")"
          return 0
        fi
      elif [[ "$line" =~ ^[[:space:]]{6}-[[:space:]](.+)$ ]]; then
        entry_value="$(_strip_wrapping_quotes "${BASH_REMATCH[1]}")"
        entry_key="${entry_value%%=*}"
        if [[ "$entry_key" == "$wanted_key" && "$entry_value" == *=* ]]; then
          printf '%s' "${entry_value#*=}"
          return 0
        fi
      elif [[ ! "$line" =~ ^[[:space:]]{6} ]]; then
        in_environment="no"
      fi
    elif [[ "$in_service" == "yes" && "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "$service_header" ]]; then
      in_service="no"
    fi

    if [[ "$line" == "$service_header" ]]; then
      in_service="yes"
      in_environment="no"
    elif [[ "$in_service" == "yes" && "$line" == "    environment:" ]]; then
      in_environment="yes"
    fi
  done < "$compose_file"

  return 1
}

# Parse the image: value for a named service from a compose file.
# Assumes wizard-standard indentation: services at 2 spaces, properties at 4 spaces.
read_service_image_value() {
  local compose_file="$1"
  local service_name="$2"
  local line
  local service_header="  ${service_name}:"
  local in_service="no"

  if [[ ! -f "$compose_file" ]]; then
    return 1
  fi

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_service" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{4}image:[[:space:]]*(.+)$ ]]; then
        printf '%s' "${BASH_REMATCH[1]}"
        return 0
      fi

      if [[ "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "$service_header" ]]; then
        in_service="no"
      elif [[ "$line" =~ ^[^[:space:]] ]]; then
        in_service="no"
      fi
    fi

    if [[ "$line" == "$service_header" ]]; then
      in_service="yes"
    fi
  done < "$compose_file"

  return 1
}

_extract_named_volume_name() {
  local mount_spec="$1"
  local source=""

  mount_spec="$(_strip_wrapping_quotes "$mount_spec")"
  source="${mount_spec%%:*}"

  if [[ "$source" == "$mount_spec" || -z "$source" ]]; then
    return 1
  fi

  if [[ "$source" == .* || "$source" == /* || "$source" == "~"* ]]; then
    return 1
  fi

  if [[ "$source" == *"/"* || "$source" == *'$'* ]]; then
    return 1
  fi

  printf '%s' "$source"
}

_collect_referenced_named_volumes() {
  local compose_file="$1"
  local line
  local in_services="no"
  local current_service=""
  local in_volumes="no"
  local in_long_volume_entry="no"
  local long_volume_type=""
  local long_volume_source=""
  local volume_name=""
  local long_entry_key=""
  local long_entry_value=""
  local -A seen=()

  if [[ ! -f "$compose_file" ]]; then
    return 0
  fi

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "services:" ]]; then
      in_services="yes"
      current_service=""
      in_volumes="no"
      in_long_volume_entry="no"
      long_volume_type=""
      continue
    fi

    if [[ "$in_services" == "yes" && "$line" =~ ^[A-Za-z] && "$line" != "services:" ]]; then
      in_services="no"
      in_volumes="no"
      in_long_volume_entry="no"
      continue
    fi

    if [[ "$in_services" != "yes" ]]; then
      continue
    fi

    if [[ "$line" =~ ^[[:space:]]{2}([A-Za-z0-9_-]+):[[:space:]]*$ ]]; then
      current_service="${BASH_REMATCH[1]}"
      in_volumes="no"
      in_long_volume_entry="no"
      long_volume_type=""
      long_volume_source=""
      continue
    fi

    if [[ -z "$current_service" ]]; then
      continue
    fi

    if [[ "$line" == "    volumes:" ]]; then
      in_volumes="yes"
      in_long_volume_entry="no"
      long_volume_type=""
      long_volume_source=""
      continue
    fi

    if [[ "$in_volumes" != "yes" ]]; then
      continue
    fi

    if [[ "$line" =~ ^[[:space:]]{4}[^[:space:]-] || "$line" =~ ^[[:space:]]{2}[^[:space:]] ]]; then
      in_volumes="no"
      in_long_volume_entry="no"
      long_volume_type=""
      long_volume_source=""
      continue
    fi

    if [[ "$line" =~ ^[[:space:]]{6}-[[:space:]](.+)$ ]]; then
      local volume_entry="${BASH_REMATCH[1]}"
      volume_entry="$(_strip_wrapping_quotes "$volume_entry")"
      in_long_volume_entry="no"
      long_volume_type=""
      long_volume_source=""

      if [[ "$volume_entry" =~ ^([A-Za-z_][A-Za-z0-9_-]*):[[:space:]]*(.*)$ ]]; then
        long_entry_key="${BASH_REMATCH[1]}"
        long_entry_value="$(_strip_wrapping_quotes "${BASH_REMATCH[2]}")"
        case "$long_entry_key" in
          type|source|target|read_only|bind|volume|tmpfs|consistency|nocopy|subpath)
            in_long_volume_entry="yes"
            case "$long_entry_key" in
              type)
                if [[ "$long_entry_value" == "volume" ]]; then
                  long_volume_type="volume"
                else
                  long_volume_type="other"
                fi
                ;;
              source)
                long_volume_source="$long_entry_value"
                ;;
            esac
            if [[ "$long_volume_type" == "volume" && -n "$long_volume_source" && -z "${seen[$long_volume_source]+set}" ]]; then
              seen["$long_volume_source"]=1
              printf '%s\n' "$long_volume_source"
            fi
            continue
            ;;
        esac
      fi

      volume_name="$(_extract_named_volume_name "$volume_entry")" || continue
      if [[ -z "${seen[$volume_name]+set}" ]]; then
        seen["$volume_name"]=1
        printf '%s\n' "$volume_name"
      fi
      continue
    fi

    if [[ "$in_long_volume_entry" == "yes" ]] && \
      [[ "$line" =~ ^[[:space:]]{8}([A-Za-z_][A-Za-z0-9_-]*):[[:space:]]*(.+)$ ]]; then
      long_entry_key="${BASH_REMATCH[1]}"
      long_entry_value="$(_strip_wrapping_quotes "${BASH_REMATCH[2]}")"
      case "$long_entry_key" in
        type)
          if [[ "$long_entry_value" == "volume" ]]; then
            long_volume_type="volume"
          else
            long_volume_type="other"
          fi
          ;;
        source)
          long_volume_source="$long_entry_value"
          ;;
      esac

      if [[ "$long_volume_type" == "volume" && -n "$long_volume_source" && -z "${seen[$long_volume_source]+set}" ]]; then
        seen["$long_volume_source"]=1
        printf '%s\n' "$long_volume_source"
      fi
    fi
  done < "$compose_file"
}

_trim_trailing_blank_lines_in_file() {
  local file="$1"
  local trim_file="${file}.trim-tail"
  _FILE_OPS_CLEANUP_TMP+=("$trim_file")

  awk '
    { lines[NR] = $0 }
    END {
      last = NR
      while (last > 0 && lines[last] == "") {
        last--
      }
      for (i = 1; i <= last; i++) {
        print lines[i]
      }
    }
  ' "$file" > "$trim_file"

  mv "$trim_file" "$file"
}

_append_referenced_volume_blocks() {
  local compose_file="$1"
  local -a referenced_volumes=()
  local volume_name
  local root_service

  while IFS= read -r volume_name; do
    if [[ -n "$volume_name" ]]; then
      referenced_volumes+=("$volume_name")
    fi
  done < <(_collect_referenced_named_volumes "$compose_file")

  if ((${#referenced_volumes[@]} == 0)); then
    return 0
  fi

  _trim_trailing_blank_lines_in_file "$compose_file"
  printf '\nvolumes:\n' >> "$compose_file"
  for volume_name in "${referenced_volumes[@]}"; do
    if _is_wizard_managed_volume_name "$volume_name"; then
      root_service="$(_managed_volume_root_name "$volume_name")"
      if [[ -n "${_FILE_OPS_VOLUME_BLOCKS[$volume_name]+set}" ]] && \
        _should_preserve_wizard_managed_root_service "$root_service"; then
        printf '%s' "${_FILE_OPS_VOLUME_BLOCKS[$volume_name]}" >> "$compose_file"
      else
        printf '  %s:\n' "$volume_name" >> "$compose_file"
      fi
    elif [[ -n "${_FILE_OPS_VOLUME_BLOCKS[$volume_name]+set}" ]]; then
      printf '%s' "${_FILE_OPS_VOLUME_BLOCKS[$volume_name]}" >> "$compose_file"
    else
      printf '  %s:\n' "$volume_name" >> "$compose_file"
    fi
  done
}

generate_docker_compose() {
  local output_file="${1:-${REPO_ROOT:-.}/docker-compose.yml}"
  local base_file="${REPO_ROOT:-.}/docker-compose.yml"
  local tmp_file="${output_file}.tmp"
  local service_blocks_file="${output_file}.services"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  _FILE_OPS_CLEANUP_TMP+=("$service_blocks_file")
  local template_file
  local lightrag_mounts=()
  local lightrag_env_entries=()
  local key
  local root_service

  # Prefer the existing generated compose as the starting point to preserve
  # any user customisations to the lightrag service.  Fall back to the base
  # docker-compose.yml when the output file doesn't exist yet.
  if [[ -f "$output_file" && "$output_file" != "$base_file" ]]; then
    _refresh_existing_managed_root_service_set_from_compose "$output_file"
    _collect_top_level_volume_blocks "$output_file"
    cp "$output_file" "$tmp_file"
    # Strip wizard-managed services and top-level volumes. User-managed
    # services are preserved, while volumes are rebuilt from final service
    # references after managed templates are appended.
    _strip_wizard_managed_services_and_top_level_volumes "$tmp_file"
  elif [[ -f "$base_file" ]]; then
    _refresh_existing_managed_root_service_set_from_compose "$base_file"
    _collect_top_level_volume_blocks "$base_file"
    cp "$base_file" "$tmp_file"
    _strip_wizard_managed_services_and_top_level_volumes "$tmp_file"
  else
    EXISTING_MANAGED_ROOT_SERVICE_SET=()
    _FILE_OPS_VOLUME_BLOCKS=()
    _FILE_OPS_VOLUME_ORDER=()
    printf 'services:\n' > "$tmp_file"
  fi

  prepare_lightrag_service_for_generated_compose "$tmp_file"
  normalize_lightrag_restart_policy "$tmp_file"
  # Remove stale wizard-managed keys from lightrag's environment so that
  # keys no longer in COMPOSE_ENV_OVERRIDES are not left behind.
  _strip_lightrag_wizard_environment_keys "$tmp_file"

  # Remove stale wizard-managed bind mounts from lightrag's volumes so that
  # mounts no longer needed (e.g. after SSL removal) are not left behind.
  _strip_lightrag_wizard_bind_mounts "$tmp_file"

  if [[ -n "${LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING:-}" ]]; then
    _strip_lightrag_wizard_ports "$tmp_file"
    inject_lightrag_port_mapping "$tmp_file" "$LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING"
  fi

  # Ensure the optional prompts directory mount exists so users can supply
  # custom entity-type prompt profiles without rebuilding the image. Mirrors
  # the ./data/inputs and ./data/rag_storage bind layout.
  if ! _lightrag_volumes_have_container_target "$tmp_file" "/app/data/prompts"; then
    lightrag_mounts+=("./data/prompts:/app/data/prompts")
  fi

  append_lightrag_ssl_mount lightrag_mounts "${COMPOSE_ENV_OVERRIDES[SSL_CERTFILE]:-}" || return 1
  append_lightrag_ssl_mount lightrag_mounts "${COMPOSE_ENV_OVERRIDES[SSL_KEYFILE]:-}" || return 1
  if ((${#lightrag_mounts[@]} > 0)); then
    inject_lightrag_bind_mounts "$tmp_file" "${lightrag_mounts[@]}"
  fi

  for key in "${!COMPOSE_ENV_OVERRIDES[@]}"; do
    lightrag_env_entries+=("${key}=${COMPOSE_ENV_OVERRIDES[$key]}")
  done
  if ((${#lightrag_env_entries[@]} > 0)); then
    inject_lightrag_environment_overrides "$tmp_file" "${lightrag_env_entries[@]}"
  fi

  repair_misplaced_lightrag_depends_on "$tmp_file"
  inject_lightrag_depends_on "$tmp_file" "${DOCKER_SERVICES[@]}"

  : > "$service_blocks_file"
  for service in "${DOCKER_SERVICES[@]}"; do
    root_service="$(_managed_service_root_name "$service")"
    if _should_preserve_wizard_managed_root_service "$root_service" && \
      _existing_managed_root_service_present "$root_service"; then
      continue
    fi

    template_file="$TEMPLATES_DIR/${service}.yml"
    if [[ "$service" == "milvus" ]]; then
      if [[ "${ENV_VALUES[MILVUS_DEVICE]:-cpu}" == "cuda" ]]; then
        if [[ -f "$TEMPLATES_DIR/${service}-gpu.yml" ]]; then
          template_file="$TEMPLATES_DIR/${service}-gpu.yml"
        fi
      fi
    fi
    if [[ "$service" == "qdrant" ]]; then
      if [[ "${ENV_VALUES[QDRANT_DEVICE]:-cpu}" == "cuda" ]]; then
        if [[ -f "$TEMPLATES_DIR/${service}-gpu.yml" ]]; then
          template_file="$TEMPLATES_DIR/${service}-gpu.yml"
        fi
      fi
    fi
    if [[ "$service" == "vllm-rerank" ]]; then
      if [[ "${ENV_VALUES[VLLM_RERANK_DEVICE]:-cpu}" == "cuda" ]]; then
        if [[ -f "$TEMPLATES_DIR/${service}-gpu.yml" ]]; then
          template_file="$TEMPLATES_DIR/${service}-gpu.yml"
        fi
      fi
    fi
    if [[ "$service" == "vllm-embed" ]]; then
      if [[ "${ENV_VALUES[VLLM_EMBED_DEVICE]:-cpu}" == "cuda" ]]; then
        if [[ -f "$TEMPLATES_DIR/${service}-gpu.yml" ]]; then
          template_file="$TEMPLATES_DIR/${service}-gpu.yml"
        fi
      fi
    fi
    if [[ ! -f "$template_file" ]]; then
      format_error "Missing docker template: $template_file" "Reinstall the setup scripts."
      return 1
    fi

    printf '\n' >> "$service_blocks_file"
    cat "$template_file" >> "$service_blocks_file"

    case "$service" in
      postgres)
        inject_service_environment_overrides "$service_blocks_file" "postgres" \
          "POSTGRES_USER=${ENV_VALUES[POSTGRES_USER]:-}" \
          "POSTGRES_PASSWORD=${ENV_VALUES[POSTGRES_PASSWORD]:-}" \
          "POSTGRES_DB=${ENV_VALUES[POSTGRES_DATABASE]:-}"
        ;;
      neo4j)
        inject_service_environment_overrides "$service_blocks_file" "neo4j" \
          "NEO4J_AUTH=${_COMPOSE_RAW_VALUE_PREFIX}\${NEO4J_USERNAME:?missing}/\${NEO4J_PASSWORD:?missing}" \
          "NEO4J_dbms_default__database=${ENV_VALUES[NEO4J_DATABASE]:-neo4j}"
        ;;
      mongodb)
        ;;
      redis)
        ;;
      milvus)
        ;;
      qdrant)
        ;;
      memgraph)
        ;;
      opensearch)
        ;;
      vllm-rerank)
        ;;
      vllm-embed)
        ;;
    esac

    if [[ -n "${COMPOSE_SERVICE_IMAGE_OVERRIDES[$service]+set}" ]]; then
      inject_service_image_override \
        "$service_blocks_file" \
        "$service" \
        "${COMPOSE_SERVICE_IMAGE_OVERRIDES[$service]}"
    fi
  done

  _merge_managed_service_blocks "$tmp_file" "$service_blocks_file"
  _normalize_services_section_spacing "$tmp_file"
  _append_referenced_volume_blocks "$tmp_file"

  mv "$tmp_file" "$output_file"
}

prepare_lightrag_service_for_generated_compose() {
  # Let the containerized app read the mounted .env itself. Keeping env_file
  # here would make Docker Compose re-parse the same secrets and expand '$'.
  local compose_file="$1"
  local tmp_file="${compose_file}.strip-env-file"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_lightrag="no"
  local in_env_file="no"

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_env_file" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{6}-[[:space:]] ]]; then
        continue
      fi
      in_env_file="no"
    fi

    if [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]]; then
      in_lightrag="no"
    fi

    if [[ "$in_lightrag" == "yes" && "$line" == "    env_file:" ]]; then
      in_env_file="yes"
      continue
    fi

    if [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{4}container_name: ]]; then
      continue
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_env_file="no"
    fi
  done < "$compose_file"

  mv "$tmp_file" "$compose_file"
}

normalize_lightrag_restart_policy() {
  local compose_file="$1"
  local tmp_file="${compose_file}.normalize-lightrag-restart"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_lightrag="no"
  local in_deploy="no"
  local deploy_seen="no"
  local insert_blank_after_deploy="no"
  local skip_blank_after_removed_restart="no"
  local -a deploy_lines=()

  _trim_trailing_blank_lines() {
    local file="$1"
    local trim_file="${file}.trim"
    _FILE_OPS_CLEANUP_TMP+=("$trim_file")

    awk '
      { lines[NR] = $0 }
      END {
        last = NR
        while (last > 0 && lines[last] == "") {
          last--
        }
        for (i = 1; i <= last; i++) {
          print lines[i]
        }
      }
    ' "$file" > "$trim_file"

    mv "$trim_file" "$file"
  }

  _write_normalized_lightrag_deploy_block() {
    local deploy_line
    local skipping_restart_policy="no"

    printf '    deploy:\n' >> "$tmp_file"
    for deploy_line in "${deploy_lines[@]}"; do
      if [[ -z "$deploy_line" ]]; then
        continue
      fi

      if [[ "$skipping_restart_policy" == "yes" ]]; then
        if [[ "$deploy_line" =~ ^[[:space:]]{8} ]]; then
          continue
        fi
        skipping_restart_policy="no"
      fi

      if [[ "$deploy_line" == "      restart_policy:" ]]; then
        skipping_restart_policy="yes"
        continue
      fi

      printf '%s\n' "$deploy_line" >> "$tmp_file"
    done

    printf '      restart_policy:\n' >> "$tmp_file"
    printf '        condition: on-failure\n' >> "$tmp_file"
    printf '        max_attempts: 10\n' >> "$tmp_file"
  }

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_deploy" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{6} || -z "$line" ]]; then
        deploy_lines+=("$line")
        continue
      fi

      _trim_trailing_blank_lines "$tmp_file"
      _write_normalized_lightrag_deploy_block
      deploy_lines=()
      in_deploy="no"
      if [[ "$line" =~ ^[[:space:]]{2}[^[:space:]] || "$line" =~ ^[^[:space:]] ]]; then
        insert_blank_after_deploy="yes"
      fi
    fi

    if [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]] || \
      [[ "$in_lightrag" == "yes" && "$line" =~ ^[^[:space:]] ]]; then
      if [[ "$deploy_seen" != "yes" ]]; then
        _trim_trailing_blank_lines "$tmp_file"
        _write_normalized_lightrag_deploy_block
        insert_blank_after_deploy="yes"
      fi
      in_lightrag="no"
      deploy_seen="no"
      skip_blank_after_removed_restart="no"
    fi

    if [[ "$in_lightrag" == "yes" && "$line" == "    deploy:" ]]; then
      in_deploy="yes"
      deploy_seen="yes"
      deploy_lines=()
      continue
    fi

    if [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{4}restart: ]]; then
      skip_blank_after_removed_restart="yes"
      continue
    fi

    if [[ "$skip_blank_after_removed_restart" == "yes" && "$in_lightrag" == "yes" ]]; then
      if [[ -z "$line" ]]; then
        continue
      fi
      skip_blank_after_removed_restart="no"
    fi

    if [[ "$insert_blank_after_deploy" == "yes" ]]; then
      printf '\n' >> "$tmp_file"
      insert_blank_after_deploy="no"
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_deploy="no"
      deploy_seen="no"
      insert_blank_after_deploy="no"
      skip_blank_after_removed_restart="no"
      deploy_lines=()
    fi
  done < "$compose_file"

  if [[ "$in_deploy" == "yes" ]]; then
    _trim_trailing_blank_lines "$tmp_file"
    _write_normalized_lightrag_deploy_block
    deploy_seen="yes"
  fi

  if [[ "$in_lightrag" == "yes" && "$deploy_seen" != "yes" ]]; then
    _trim_trailing_blank_lines "$tmp_file"
    _write_normalized_lightrag_deploy_block
  fi

  mv "$tmp_file" "$compose_file"
}

append_lightrag_ssl_mount() {
  local array_name="$1"
  local container_path="$2"
  local relative_host_path=""
  local mount_entry=""

  if [[ -z "$container_path" ]]; then
    return 0
  fi

  if [[ "$container_path" != /app/data/* ]]; then
    format_error "Unsupported SSL path: ${container_path}" "Use paths staged under /app/data."
    return 1
  fi

  relative_host_path="./data/${container_path#/app/data/}"
  mount_entry="${relative_host_path}:${container_path}:ro"
  local -n _arr_ref="$array_name"
  _arr_ref+=("$mount_entry")
}

format_yaml_value() {
  local value="$1"
  local escaped="${value//\\/\\\\}"

  escaped="${escaped//\"/\\\"}"
  escaped="${escaped//\$/\$\$}"
  printf '"%s"' "$escaped"
}

_COMPOSE_RAW_VALUE_PREFIX="__LIGHTRAG_RAW_COMPOSE__:"

format_compose_environment_value() {
  local value="$1"

  if [[ "$value" == "${_COMPOSE_RAW_VALUE_PREFIX}"* ]]; then
    printf '%s' "${value#${_COMPOSE_RAW_VALUE_PREFIX}}"
    return 0
  fi

  format_yaml_value "$value"
}

inject_service_environment_overrides() {
  local compose_file="$1"
  local service_name="$2"
  shift 2
  local entries=("$@")
  local tmp_file="${compose_file}.${service_name}.env"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line key value
  local in_service="no"
  local in_environment="no"
  local environment_style="mapping"
  local inserted="no"
  local service_header="  ${service_name}:"

  if ((${#entries[@]} == 0)); then
    return 0
  fi

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_service" == "yes" && "$in_environment" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{6}-[[:space:]] ]]; then
        environment_style="list"
      elif [[ "$line" =~ ^[[:space:]]{6}[A-Z0-9_]+: ]]; then
        environment_style="mapping"
      fi

      if [[ "$line" =~ ^[[:space:]]{4}[^[:space:]] || "$line" =~ ^[[:space:]]{2}[^[:space:]] || "$line" =~ ^[^[:space:]] ]]; then
        if [[ "$inserted" == "no" ]]; then
          _write_service_environment_entries "$tmp_file" "$environment_style" "${entries[@]}"
          inserted="yes"
        fi
        in_environment="no"
      fi
    elif [[ "$in_service" == "yes" && \
            ( "$line" =~ ^[[:space:]]{2}[^[:space:]] || "$line" =~ ^[^[:space:]] ) && \
            "$line" != "$service_header" ]]; then
      if [[ "$inserted" == "no" ]]; then
        printf '    environment:\n' >> "$tmp_file"
        _write_service_environment_entries "$tmp_file" "mapping" "${entries[@]}"
        inserted="yes"
      fi
      in_service="no"
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "$service_header" ]]; then
      in_service="yes"
      in_environment="no"
      environment_style="mapping"
    elif [[ "$in_service" == "yes" && "$line" == "    environment:" ]]; then
      in_environment="yes"
      environment_style="mapping"
    fi
  done < "$compose_file"

  if [[ "$in_service" == "yes" && "$inserted" == "no" ]]; then
    if [[ "$in_environment" != "yes" ]]; then
      printf '    environment:\n' >> "$tmp_file"
    fi
    _write_service_environment_entries "$tmp_file" "$environment_style" "${entries[@]}"
  fi

  mv "$tmp_file" "$compose_file"
}

inject_service_image_override() {
  local compose_file="$1"
  local service_name="$2"
  local image_value="$3"
  local tmp_file="${compose_file}.${service_name}.image"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_service="no"
  local inserted="no"
  local service_header="  ${service_name}:"

  if [[ -z "$image_value" || ! -f "$compose_file" ]]; then
    return 0
  fi

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_service" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{4}image:[[:space:]]*.+$ ]]; then
        printf '    image: %s\n' "$image_value" >> "$tmp_file"
        inserted="yes"
        continue
      fi

      if [[ "$inserted" == "no" && "$line" =~ ^[[:space:]]{4}[^[:space:]] ]]; then
        printf '    image: %s\n' "$image_value" >> "$tmp_file"
        inserted="yes"
      fi

      if [[ "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "$service_header" ]]; then
        if [[ "$inserted" == "no" ]]; then
          printf '    image: %s\n' "$image_value" >> "$tmp_file"
          inserted="yes"
        fi
        in_service="no"
      elif [[ "$line" =~ ^[^[:space:]] ]]; then
        if [[ "$inserted" == "no" ]]; then
          printf '    image: %s\n' "$image_value" >> "$tmp_file"
          inserted="yes"
        fi
        in_service="no"
      fi
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "$service_header" ]]; then
      in_service="yes"
    fi
  done < "$compose_file"

  if [[ "$in_service" == "yes" && "$inserted" == "no" ]]; then
    printf '    image: %s\n' "$image_value" >> "$tmp_file"
  fi

  mv "$tmp_file" "$compose_file"
}

# Return success when the lightrag service already has a volume mount whose
# container target matches the supplied path. Used to make idempotent mount
# injections that should not duplicate existing user/wizard entries.
_lightrag_volumes_have_container_target() {
  local compose_file="$1"
  local target_path="$2"
  local line
  local in_lightrag="no"
  local in_volumes="no"
  local mount_spec=""
  local remainder=""
  local container_path=""

  if [[ ! -f "$compose_file" || -z "$target_path" ]]; then
    return 1
  fi

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_lightrag" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]] || \
         [[ "$line" =~ ^[^[:space:]] ]]; then
        in_lightrag="no"
        in_volumes="no"
      elif [[ "$line" == "    volumes:" ]]; then
        in_volumes="yes"
      elif [[ "$in_volumes" == "yes" && "$line" =~ ^[[:space:]]{4}[^[:space:]-] ]]; then
        in_volumes="no"
      elif [[ "$in_volumes" == "yes" && "$line" =~ ^[[:space:]]{6}-[[:space:]](.+)$ ]]; then
        mount_spec="$(_strip_wrapping_quotes "${BASH_REMATCH[1]}")"
        remainder="${mount_spec#*:}"
        if [[ "$remainder" == "$mount_spec" ]]; then
          continue
        fi
        container_path="${remainder%%:*}"
        if [[ "$container_path" == "$target_path" ]]; then
          return 0
        fi
      fi
    fi

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_volumes="no"
    fi
  done < "$compose_file"

  return 1
}

# Return success when a volume mount entry is a wizard-managed SSL cert/key
# bind mount (./data/certs/* -> /app/data/certs/*, optional :ro suffix).
_is_wizard_ssl_bind_mount() {
  local mount_spec="$1"
  local host_path=""
  local remainder=""
  local container_path=""
  local mode=""
  local host_suffix=""
  local container_suffix=""

  host_path="${mount_spec%%:*}"
  remainder="${mount_spec#*:}"
  if [[ "$remainder" == "$mount_spec" ]]; then
    return 1
  fi

  container_path="${remainder%%:*}"
  if [[ "$container_path" == "$remainder" ]]; then
    mode=""
  else
    mode="${remainder#${container_path}:}"
  fi

  if [[ "$host_path" != ./data/certs/* ]]; then
    return 1
  fi

  if [[ "$container_path" != /app/data/certs/* ]]; then
    return 1
  fi

  # Wizard-generated SSL mounts are read-only. Keep non-read-only mounts so
  # user-defined overrides under /app/data/certs are not stripped.
  if [[ -n "$mode" && "$mode" != "ro" ]]; then
    return 1
  fi

  host_suffix="${host_path#./data/certs/}"
  container_suffix="${container_path#/app/data/certs/}"
  [[ "$host_suffix" == "$container_suffix" ]]
}

# Remove wizard-managed SSL bind mounts from the lightrag service's volumes
# block, leaving persistent and user-added /app/data/* mounts intact.
_strip_lightrag_wizard_bind_mounts() {
  local compose_file="$1"
  local tmp_file="${compose_file}.strip-mounts"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local mount_spec
  local in_lightrag="no"
  local in_volumes="no"

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_lightrag" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]]; then
        in_lightrag="no"
        in_volumes="no"
      elif [[ "$line" == "    volumes:" ]]; then
        in_volumes="yes"
      elif [[ "$in_volumes" == "yes" ]]; then
        if [[ "$line" =~ ^[[:space:]]{4}[^[:space:]] ]]; then
          in_volumes="no"
        elif [[ "$line" =~ ^[[:space:]]{6}-[[:space:]] ]]; then
          mount_spec="${line#      - }"
          mount_spec="${mount_spec%\"}"
          mount_spec="${mount_spec#\"}"
          mount_spec="${mount_spec%\'}"
          mount_spec="${mount_spec#\'}"
          if _is_wizard_ssl_bind_mount "$mount_spec"; then
            continue
          fi
        fi
      fi
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_volumes="no"
    fi
  done < "$compose_file"

  mv "$tmp_file" "$compose_file"
}

_is_wizard_lightrag_port_mapping() {
  local port_spec="$(_strip_wrapping_quotes "$1")"

  if [[ "$port_spec" == '${HOST:-0.0.0.0}:${PORT:-9621}:9621' || \
        "$port_spec" == '${PORT:-9621}:9621' ]]; then
    return 0
  fi

  case "$port_spec" in
    9621|9621/tcp|*:9621|*:9621/tcp)
      return 0
      ;;
  esac

  return 1
}

_strip_lightrag_wizard_ports() {
  local compose_file="$1"
  local tmp_file="${compose_file}.strip-lightrag-ports"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local port_spec=""
  local in_lightrag="no"
  local in_ports="no"

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_lightrag" == "yes" && "$in_ports" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{6}-[[:space:]](.+)$ ]]; then
        port_spec="${BASH_REMATCH[1]}"
        if _is_wizard_lightrag_port_mapping "$port_spec"; then
          continue
        fi
      elif [[ ! "$line" =~ ^[[:space:]]{6} ]]; then
        in_ports="no"
      fi
    elif [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]]; then
      in_lightrag="no"
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_ports="no"
    elif [[ "$in_lightrag" == "yes" && "$line" == "    ports:" ]]; then
      in_ports="yes"
    fi
  done < "$compose_file"

  mv "$tmp_file" "$compose_file"
}

inject_lightrag_bind_mounts() {
  local compose_file="$1"
  shift
  local mounts=("$@")
  local tmp_file="${compose_file}.mounts"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_lightrag="no"
  local in_volumes="no"
  local inserted="no"

  if ((${#mounts[@]} == 0)); then
    return 0
  fi

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_lightrag" == "yes" && "$in_volumes" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{4}[^[:space:]-] || "$line" =~ ^[[:space:]]{2}[^[:space:]] || "$line" =~ ^(volumes|networks): ]]; then
        if [[ "$inserted" == "no" ]]; then
          for mount in "${mounts[@]}"; do
            printf '      - %s\n' "$mount" >> "$tmp_file"
          done
          inserted="yes"
        fi
        in_volumes="no"
      fi
    elif [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]]; then
      if [[ "$inserted" == "no" ]]; then
        printf '    volumes:\n' >> "$tmp_file"
        for mount in "${mounts[@]}"; do
          printf '      - %s\n' "$mount" >> "$tmp_file"
        done
        inserted="yes"
      fi
      in_lightrag="no"
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_volumes="no"
    elif [[ "$in_lightrag" == "yes" && "$line" == "    volumes:" ]]; then
      in_volumes="yes"
    fi
  done < "$compose_file"

  if [[ "$in_lightrag" == "yes" && "$inserted" == "no" ]]; then
    if [[ "$in_volumes" != "yes" ]]; then
      printf '    volumes:\n' >> "$tmp_file"
    fi
    for mount in "${mounts[@]}"; do
      printf '      - %s\n' "$mount" >> "$tmp_file"
    done
  fi

  mv "$tmp_file" "$compose_file"
}

inject_lightrag_port_mapping() {
  local compose_file="$1"
  local port_mapping="$2"
  local tmp_file="${compose_file}.ports"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_lightrag="no"
  local in_ports="no"
  local inserted="no"

  if [[ -z "$port_mapping" ]]; then
    return 0
  fi

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_lightrag" == "yes" && "$in_ports" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{4}[^[:space:]-] || "$line" =~ ^[[:space:]]{2}[^[:space:]] || "$line" =~ ^(volumes|networks): ]]; then
        if [[ "$inserted" == "no" ]]; then
          printf '      - "%s"\n' "$port_mapping" >> "$tmp_file"
          inserted="yes"
        fi
        in_ports="no"
      fi
    elif [[ "$in_lightrag" == "yes" && "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ]]; then
      if [[ "$inserted" == "no" ]]; then
        printf '    ports:\n' >> "$tmp_file"
        printf '      - "%s"\n' "$port_mapping" >> "$tmp_file"
        inserted="yes"
      fi
      in_lightrag="no"
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      in_ports="no"
    elif [[ "$in_lightrag" == "yes" && "$line" == "    ports:" ]]; then
      in_ports="yes"
    fi
  done < "$compose_file"

  if [[ "$in_lightrag" == "yes" && "$inserted" == "no" ]]; then
    if [[ "$in_ports" != "yes" ]]; then
      printf '    ports:\n' >> "$tmp_file"
    fi
    printf '      - "%s"\n' "$port_mapping" >> "$tmp_file"
  fi

  mv "$tmp_file" "$compose_file"
}

repair_misplaced_lightrag_depends_on() {
  local compose_file="$1"
  local tmp_file="${compose_file}.repair-lightrag-depends-on"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_services="no"
  local in_lightrag="no"
  local lightrag_has_depends_on="no"
  local candidate_service=""
  local candidate_root_service=""
  local captured_block=""
  local candidate_header=""
  local inserted="no"
  local in_candidate_service="no"
  local skipping_candidate_depends_on="no"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "services:" ]]; then
      in_services="yes"
      continue
    fi

    if [[ "$in_services" == "yes" && "$line" =~ ^[^[:space:]] && "$line" != "services:" ]]; then
      break
    fi

    if [[ "$in_services" != "yes" ]]; then
      continue
    fi

    if [[ "$in_lightrag" == "yes" ]]; then
      if [[ "$line" == "    depends_on:" ]]; then
        lightrag_has_depends_on="yes"
        break
      fi

      if [[ "$line" =~ ^[[:space:]]{2}([A-Za-z0-9_-]+):[[:space:]]*$ ]] && \
        [[ "${BASH_REMATCH[1]}" != "lightrag" ]]; then
        candidate_service="${BASH_REMATCH[1]}"
        candidate_root_service="$(_managed_service_root_name "$candidate_service")"
        if [[ -z "$candidate_root_service" || "$candidate_root_service" == "milvus" ]]; then
          break
        fi
        in_lightrag="no"
      fi
      continue
    fi

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      continue
    fi

    if [[ -n "$candidate_service" ]]; then
      if [[ "$line" == "    depends_on:" ]]; then
        captured_block="    depends_on:"$'\n'
        continue
      fi

      if [[ -n "$captured_block" ]]; then
        if [[ "$line" =~ ^[[:space:]]{6} ]]; then
          captured_block+="${line}"$'\n'
          continue
        fi
        break
      fi

      if [[ "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  ${candidate_service}:" ]]; then
        break
      fi

      if [[ "$line" =~ ^[^[:space:]] ]]; then
        break
      fi
    fi
  done < "$compose_file"

  if [[ "$lightrag_has_depends_on" == "yes" || -z "$captured_block" || -z "$candidate_service" ]]; then
    return 0
  fi

  candidate_header="  ${candidate_service}:"
  : > "$tmp_file"
  in_lightrag="no"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$skipping_candidate_depends_on" == "yes" ]]; then
      if [[ "$line" =~ ^[[:space:]]{6} ]]; then
        continue
      fi
      skipping_candidate_depends_on="no"
    fi

    if [[ "$in_lightrag" == "yes" && "$inserted" == "no" ]] && \
      [[ ( "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "  lightrag:" ) || "$line" =~ ^[^[:space:]] ]]; then
      printf '%s' "$captured_block" >> "$tmp_file"
      inserted="yes"
      in_lightrag="no"
    fi

    if [[ "$line" == "$candidate_header" ]]; then
      in_candidate_service="yes"
    elif [[ "$in_candidate_service" == "yes" ]] && \
      [[ ( "$line" =~ ^[[:space:]]{2}[^[:space:]] && "$line" != "$candidate_header" ) || "$line" =~ ^[^[:space:]] ]]; then
      in_candidate_service="no"
    fi

    if [[ "$in_candidate_service" == "yes" && "$line" == "    depends_on:" ]]; then
      skipping_candidate_depends_on="yes"
      continue
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
    fi
  done < "$compose_file"

  if [[ "$in_lightrag" == "yes" && "$inserted" == "no" ]]; then
    printf '%s' "$captured_block" >> "$tmp_file"
  fi

  mv "$tmp_file" "$compose_file"
}

inject_lightrag_environment_overrides() {
  local compose_file="$1"
  shift
  inject_service_environment_overrides "$compose_file" "lightrag" "$@"
}

inject_lightrag_depends_on() {
  local compose_file="$1"
  shift
  local candidate_service
  local managed_services=()
  local tmp_file="${compose_file}.depends-on"
  _FILE_OPS_CLEANUP_TMP+=("$tmp_file")
  local line
  local in_lightrag="no"
  local in_depends_on="no"
  local inserted="no"
  local insert_blank_after_depends_on="no"
  local current_dep_name=""
  local current_dep_block=""
  local dep_name=""
  local dep_tail=""
  local dep_service=""
  declare -A preserved_dep_blocks=()
  declare -A preserved_dep_seen=()
  local -a preserved_dep_order=()

  for candidate_service in "$@"; do
    if _is_wizard_managed_root_service_name "$candidate_service"; then
      managed_services+=("$candidate_service")
    fi
  done

  _record_preserved_depends_on_entry() {
    local service_name="$1"
    local block="$2"

    if [[ -z "$service_name" ]] || _is_wizard_managed_root_service_name "$service_name"; then
      return 0
    fi

    if [[ -n "${preserved_dep_seen[$service_name]+set}" ]]; then
      return 0
    fi

    preserved_dep_seen["$service_name"]=1
    preserved_dep_order+=("$service_name")
    preserved_dep_blocks["$service_name"]="$block"
  }

  _flush_current_depends_on_entry() {
    if [[ -z "$current_dep_name" ]]; then
      return 0
    fi

    _record_preserved_depends_on_entry "$current_dep_name" "$current_dep_block"
    current_dep_name=""
    current_dep_block=""
  }

  _trim_trailing_blank_lines() {
    local file="$1"
    local trim_file="${file}.trim"
    _FILE_OPS_CLEANUP_TMP+=("$trim_file")

    awk '
      { lines[NR] = $0 }
      END {
        last = NR
        while (last > 0 && lines[last] == "") {
          last--
        }
        for (i = 1; i <= last; i++) {
          print lines[i]
        }
      }
    ' "$file" > "$trim_file"

    mv "$trim_file" "$file"
  }

  _write_lightrag_depends_on_block() {
    local managed_service
    local preserved_service

    if ((${#preserved_dep_order[@]} == 0 && ${#managed_services[@]} == 0)); then
      inserted="yes"
      return 0
    fi

    printf '    depends_on:\n' >> "$tmp_file"

    for preserved_service in "${preserved_dep_order[@]}"; do
      printf '%s' "${preserved_dep_blocks[$preserved_service]}" >> "$tmp_file"
    done

    for managed_service in "${managed_services[@]}"; do
      printf '      %s:\n' "$managed_service" >> "$tmp_file"
      printf '        condition: service_healthy\n' >> "$tmp_file"
    done

    inserted="yes"
  }

  : > "$tmp_file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$in_depends_on" == "yes" ]]; then
      if [[ -n "$current_dep_name" && "$line" =~ ^[[:space:]]{8} ]]; then
        current_dep_block+="${line}"$'\n'
        continue
      fi

      if [[ "$line" =~ ^[[:space:]]{6}-[[:space:]](.+)$ ]]; then
        _flush_current_depends_on_entry
        dep_service="$(_strip_wrapping_quotes "${BASH_REMATCH[1]}")"
        _record_preserved_depends_on_entry \
          "$dep_service" \
          "$(printf '      %s:\n        condition: service_started\n' "$dep_service")"
        continue
      fi

      if [[ "$line" =~ ^[[:space:]]{6}([A-Za-z0-9_.-]+):[[:space:]]*(.*)$ ]]; then
        _flush_current_depends_on_entry
        dep_name="${BASH_REMATCH[1]}"
        dep_tail="${BASH_REMATCH[2]}"
        current_dep_name="$(_strip_wrapping_quotes "$dep_name")"
        if [[ -n "$dep_tail" ]]; then
          current_dep_block="      ${current_dep_name}: ${dep_tail}"$'\n'
        else
          current_dep_block="      ${current_dep_name}:"$'\n'
        fi
        continue
      fi

      _flush_current_depends_on_entry
      if [[ "$inserted" == "no" ]]; then
        _trim_trailing_blank_lines "$tmp_file"
        _write_lightrag_depends_on_block
      fi
      in_depends_on="no"
    fi

    if [[ "$in_lightrag" == "yes" && "$line" == "    depends_on:" ]]; then
      in_depends_on="yes"
      continue
    fi

    if [[ "$in_lightrag" == "yes" && \
          ( "$line" =~ ^[[:space:]]{2}[^[:space:]] || "$line" =~ ^[^[:space:]] ) && \
          "$line" != "  lightrag:" ]]; then
      if [[ "$inserted" == "no" ]]; then
        _trim_trailing_blank_lines "$tmp_file"
        _write_lightrag_depends_on_block
        insert_blank_after_depends_on="yes"
      fi
      in_lightrag="no"
    fi

    if [[ "$insert_blank_after_depends_on" == "yes" ]]; then
      printf '\n' >> "$tmp_file"
      insert_blank_after_depends_on="no"
    fi

    printf '%s\n' "$line" >> "$tmp_file"

    if [[ "$line" == "  lightrag:" ]]; then
      in_lightrag="yes"
      inserted="no"
      insert_blank_after_depends_on="no"
      in_depends_on="no"
      current_dep_name=""
      current_dep_block=""
      preserved_dep_blocks=()
      preserved_dep_seen=()
      preserved_dep_order=()
    fi
  done < "$compose_file"

  if [[ "$in_depends_on" == "yes" ]]; then
    _flush_current_depends_on_entry
    if [[ "$inserted" == "no" ]]; then
      _trim_trailing_blank_lines "$tmp_file"
      _write_lightrag_depends_on_block
    fi
  elif [[ "$in_lightrag" == "yes" && "$inserted" == "no" ]]; then
    _trim_trailing_blank_lines "$tmp_file"
    _write_lightrag_depends_on_block
  fi

  mv "$tmp_file" "$compose_file"
}

# Find the first generated compose file in priority order.
# Prints the path if found, empty string if not.
find_generated_compose_file() {
  local repo_root="${REPO_ROOT:-.}"
  local candidates=(
    "final:$repo_root/docker-compose.final.yml"
    "development:$repo_root/docker-compose.development.yml"
    "production:$repo_root/docker-compose.production.yml"
    "custom:$repo_root/docker-compose.custom.yml"
    "local:$repo_root/docker-compose.local.yml"
  )
  local candidate f

  for candidate in "${candidates[@]}"; do
    f="${candidate#*:}"
    if [[ -f "$f" ]]; then
      printf '%s' "$f"
      return 0
    fi
  done
  printf ''
}

# Detect service names in a compose file's services: block (excluding lightrag).
# Prints one service name per line.
detect_compose_services() {
  local compose_file="$1"
  local in_services="no"
  local line

  if [[ ! -f "$compose_file" ]]; then
    return 0
  fi

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "services:" ]]; then
      in_services="yes"
      continue
    fi
    if [[ "$in_services" == "yes" ]]; then
      if [[ "$line" =~ ^[^[:space:]] && "$line" != "services:" ]]; then
        in_services="no"
        continue
      fi
      if [[ "$line" =~ ^[[:space:]]{2}([A-Za-z0-9_-]+):[[:space:]]*$ ]]; then
        local svc_name="${BASH_REMATCH[1]}"
        if [[ "$svc_name" != "lightrag" ]]; then
          printf '%s\n' "$svc_name"
        fi
      fi
    fi
  done < "$compose_file"
}

detect_managed_root_services() {
  local compose_file="$1"
  local service_name
  local root_service
  declare -A seen_roots=()

  while IFS= read -r service_name; do
    root_service="$(_managed_service_root_name "$service_name")"
    if [[ -n "$root_service" && -z "${seen_roots[$root_service]+set}" ]]; then
      seen_roots["$root_service"]=1
      printf '%s\n' "$root_service"
    fi
  done < <(detect_compose_services "$compose_file")
}
