```bash
#!/usr/bin/env bash
#
# bulk_rename.sh - Recursively rename files matching a glob pattern with a prefix and sequence numbers.
#
# Usage:
#   ./bulk_rename.sh [OPTIONS] <target_directory> <glob_pattern> <prefix>
#
# Options:
#   --dry-run       Print planned renames without executing them.
#   -h, --help      Show this help message.
#
# Examples:
#   # Dry run: preview renaming all .jpg files in ./photos to IMG_001.jpg, IMG_002.jpg, etc.
#   ./bulk_rename.sh --dry-run ./photos "*.jpg" "IMG_"
#
#   # Actually rename all .txt files in ./docs to DOC_001.txt, DOC_002.txt, etc.
#   ./bulk_rename.sh ./docs "*.txt" "DOC_"
#
#   # Handle files with spaces and unicode safely
#   ./bulk_rename.sh ./mixed "*.pdf" "Report_"
#
# Notes:
#   - Files are processed in sorted order for deterministic numbering.
#   - Files whose basename already matches the pattern <prefix><number><ext> are skipped.
#   - Zero-padded sequence numbers start at 1 and pad to at least 3 digits.
#   - Original file extensions are preserved.
#   - Requires bash 4+ for associative arrays and nullglob.

set -euo pipefail

# -----------------------------------------------------------------------------
# Usage
# -----------------------------------------------------------------------------
usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS] <target_directory> <glob_pattern> <prefix>

Recursively find files under <target_directory> matching <glob_pattern> and rename
them with <prefix> followed by a zero-padded sequence number, preserving extensions.

Options:
  --dry-run       Print planned renames without executing them.
  -h, --help      Show this help message.

Arguments:
  target_directory   Root directory to search recursively.
  glob_pattern       Glob pattern to match filenames (e.g., "*.jpg").
  prefix             Prefix to prepend to renamed files (e.g., "IMG_").

Examples:
  $(basename "$0") --dry-run ./photos "*.jpg" "IMG_"
  $(basename "$0") ./docs "*.txt" "DOC_"
EOF
    exit 0
}

# -----------------------------------------------------------------------------
# Argument parsing
# -----------------------------------------------------------------------------
dry_run=false
target_dir=""
glob_pattern=""
prefix=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --dry-run)
            dry_run=true
            shift
            ;;
        -h|--help)
            usage
            ;;
        -*)
            echo "Error: Unknown option '$1'" >&2
            usage
            ;;
        *)
            if [[ -z "$target_dir" ]]; then
                target_dir="$1"
            elif [[ -z "$glob_pattern" ]]; then
                glob_pattern="$1"
            elif [[ -z "$prefix" ]]; then
                prefix="$1"
            else
                echo "Error: Too many arguments." >&2
                usage
            fi
            shift
            ;;
    esac
done

if [[ -z "$target_dir" || -z "$glob_pattern" || -z "$prefix" ]]; then
    echo "Error: Missing required arguments." >&2
    usage
fi

if [[ ! -d "$target_dir" ]]; then
    echo "Error: Target directory '$target_dir' does not exist or is not a directory." >&2
    exit 1
fi

# -----------------------------------------------------------------------------
# Helper: Check if a filename already matches the target pattern
# -----------------------------------------------------------------------------
# Returns 0 (true) if the basename matches <prefix><digits><ext>
matches_existing_pattern() {
    local filename="$1"
    local ext="$2"
    local base="${filename%.*}"
    local file_ext="${filename##*.}"

    # If no extension, treat entire name as base
    if [[ "$base" == "$filename" ]]; then
        ext=""
        file_ext=""
    fi

    # Check if base starts with prefix and the rest is all digits
    if [[ "$base" == "$prefix"* ]]; then
        local suffix="${base#"$prefix"}"
        if [[ "$suffix" =~ ^[0-9]+$ ]]; then
            return 0
        fi
    fi
    return 1
}

# -----------------------------------------------------------------------------
# Main logic
# -----------------------------------------------------------------------------
# Collect matching files in sorted order for deterministic numbering
shopt -s nullglob globstar

# Build array of matching files
mapfile -t files < <(find "$target_dir" -type f -name "$glob_pattern" | sort)

shopt -u nullglob globstar

if [[ ${#files[@]} -eq 0 ]]; then
    echo "No files matching pattern '$glob_pattern' found under '$target_dir'."
    exit 0
fi

seq_num=1
renamed_count=0
skipped_count=0

for filepath in "${files[@]}"; do
    # Extract directory and basename
    dir="$(dirname "$filepath")"
    filename="$(basename "$filepath")"

    # Extract extension
    if [[ "$filename" == *.* ]]; then
        ext=".${filename##*.}"
    else
        ext=""
    fi

    # Check if file already matches the target pattern
    if matches_existing_pattern "$filename" "$ext"; then
        ((skipped_count++)) || true
        continue
    fi

    # Construct new filename
    # Zero-pad sequence number to at least 3 digits
    padded_num=$(printf "%03d" "$seq_num")
    new_filename="${prefix}${padded_num}${ext}"
    new_filepath="${dir}/${new_filename}"

    # Check if target already exists (collision)
    if [[ -e "$new_filepath" ]]; then
        echo "Warning: Skipping '$filepath' -> '$new_filepath' (target already exists)." >&2
        ((skipped_count++)) || true
        continue
    fi

    if [[ "$dry_run" == true ]]; then
        echo "[DRY RUN] Would rename: '$filepath' -> '$new_filepath'"
    else
        mv -- "$filepath" "$new_filepath"
        echo "Renamed: '$filepath' -> '$new_filepath'"
    fi

    ((renamed_count++)) || true
    ((seq_num++)) || true
done

echo ""
echo "Summary: ${renamed_count} file(s) renamed, ${skipped_count} file(s) skipped."

if [[ "$dry_run" == true ]]; then
    echo "(Dry run mode: no files were actually renamed.)"
fi
```