#!/usr/bin/env bash
# Credits: Peter Steinberger

set -euo pipefail
# Disable glob expansion to handle brackets in file paths
set -f
usage() {
  printf 'Usage: %s [--force] [--patch <diff-file>|--patch -] "commit message" "file" ["file" ...]\n' "$(basename "$0")" >&2
  exit 2
}

if [ "$#" -lt 2 ]; then
  usage
fi

force_delete_lock=false
patch_inputs=()

while [ "$#" -gt 0 ]; do
  case "${1:-}" in
    --force)
      force_delete_lock=true
      shift
      ;;
    --patch)
      shift
      if [ "$#" -eq 0 ]; then
        printf 'Error: --patch requires a diff file path or - for stdin\n' >&2
        exit 1
      fi
      patch_inputs+=("$1")
      shift
      ;;
    --)
      shift
      break
      ;;
    -*)
      printf 'Error: unknown option: %s\n' "$1" >&2
      usage
      ;;
    *)
      break
      ;;
  esac
done

if [ "$#" -lt 2 ]; then
  usage
fi

commit_message=$1
shift

if [[ "$commit_message" != *[![:space:]]* ]]; then
  printf 'Error: commit message must not be empty\n' >&2
  exit 1
fi

if [ -e "$commit_message" ]; then
  printf 'Error: first argument looks like a file path ("%s"); provide the commit message first\n' "$commit_message" >&2
  exit 1
fi

if [ "$#" -eq 0 ]; then
  usage
fi

files=("$@")
use_patch_mode=false
if [ "${#patch_inputs[@]}" -gt 0 ]; then
  use_patch_mode=true
fi

# Disallow "." because it stages the entire repository and defeats the helper's safety guardrails.
for file in "${files[@]}"; do
  if [ "$file" = "." ]; then
    printf 'Error: "." is not allowed; list specific paths instead\n' >&2
    exit 1
  fi
    if [ -d "$file" ]; then
    printf 'Error: "%s" is a directory; list specific files instead\n' "$file" >&2
    exit 1
  fi
done

last_commit_error=''

run_git_commit() {
  local stderr_log
  stderr_log=$(mktemp)
  if git commit -m "$commit_message" -- "${files[@]}" 2> >(tee "$stderr_log" >&2); then
    rm -f "$stderr_log"
    last_commit_error=''
    return 0
  fi

  last_commit_error=$(cat "$stderr_log")
  rm -f "$stderr_log"
  return 1
}

for file in "${files[@]}"; do
  if [ ! -e "$file" ]; then
    # Allow staging deletions: when a file is removed from disk, it may no longer exist in the index
    # (once staged for deletion), but it can still exist in HEAD.
    if ! git ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then
      if ! git cat-file -e "HEAD:$file" >/dev/null 2>&1; then
        printf 'Error: file not found: %s\n' "$file" >&2
        exit 1
      fi
    fi
  fi
done

normalize_path() {
  local value="$1"
  # Keep relative paths stable for comparison with git diff --name-only output.
  value="${value#./}"
  printf '%s\n' "$value"
}

patch_file=''
cleanup_files=()
cleanup() {
  for f in "${cleanup_files[@]}"; do
    [ -n "$f" ] && [ -f "$f" ] && rm -f "$f"
  done
}
trap cleanup EXIT

if [ "$use_patch_mode" = true ]; then
  patch_file=$(mktemp)
  cleanup_files+=("$patch_file")

  for patch_input in "${patch_inputs[@]}"; do
    if [ "$patch_input" = "-" ]; then
      cat >>"$patch_file"
    else
      if [ ! -f "$patch_input" ]; then
        printf 'Error: patch file not found: %s\n' "$patch_input" >&2
        exit 1
      fi
      cat "$patch_input" >>"$patch_file"
    fi
    printf '\n' >>"$patch_file"
  done

  if [ ! -s "$patch_file" ]; then
    printf 'Error: patch input is empty\n' >&2
    exit 1
  fi
fi

git restore --staged :/

if [ "$use_patch_mode" = true ]; then
  if ! git apply --cached -- "$patch_file"; then
    printf 'Error: failed to apply patch to index\n' >&2
    exit 1
  fi

  mapfile -t staged_files < <(git diff --cached --name-only)
  if [ "${#staged_files[@]}" -eq 0 ]; then
    printf 'Warning: no staged changes detected from patch input\n' >&2
    exit 1
  fi

  for staged in "${staged_files[@]}"; do
    staged_norm=$(normalize_path "$staged")
    allowed=false
    for file in "${files[@]}"; do
      if [ "$staged_norm" = "$(normalize_path "$file")" ]; then
        allowed=true
        break
      fi
    done
    if [ "$allowed" = false ]; then
      printf 'Error: patch staged unexpected file: %s\n' "$staged_norm" >&2
      printf 'Allowed files: %s\n' "${files[*]}" >&2
      exit 1
    fi
  done
else
  git add -A -- "${files[@]}"
fi

if git diff --staged --quiet; then
  printf 'Warning: no staged changes detected for: %s\n' "${files[*]}" >&2
  exit 1
fi

committed=false
if run_git_commit; then
  committed=true
elif [ "$force_delete_lock" = true ]; then
  lock_path=$(
    printf '%s\n' "$last_commit_error" |
      awk -F"'" '/Unable to create .*\.git\/index\.lock/ { print $2; exit }'
  )

  if [ -n "$lock_path" ] && [ -e "$lock_path" ]; then
    rm -f "$lock_path"
    printf 'Removed stale git lock: %s\n' "$lock_path" >&2
    if run_git_commit; then
      committed=true
    fi
  fi
fi

if [ "$committed" = false ]; then
  exit 1
fi

printf 'Committed "%s" with %d files\n' "$commit_message" "${#files[@]}"
