#!/usr/bin/env bash

set -u
set -o pipefail

PROGRAM="$(basename "$0")"
WORK_ROOT="$(pwd -P)"
REPO_URL="${CIVIDESK_CIVICRM_REPO_URL:-https://github.com/civicrm/civicrm-core}"
TMP_ROOT="/tmp"
VERSION=""
SUCCESS_LIST=""
FAILURE_REPORT=""

usage() {
  cat <<EOF
Usage: $PROGRAM [OPTIONS]

Check CiviCRM core patches against a clean checkout and report which patches
apply successfully. For unsuccessful patches, print the rejected chunks.

Options:
  -v, --version VERSION   CiviCRM version/branch to use.
                          Defaults to the latest patches-[version] directory.
  -t, --tmp-dir DIR       Directory containing civicrm-core-VERSION.
                          Defaults to /tmp.
      --repo-url URL      CiviCRM core repository URL.
                          Defaults to $REPO_URL
  -h, --help              Show this help.
EOF
}

log() {
  printf '%s\n' "$*"
}

warn() {
  printf 'Warning: %s\n' "$*" >&2
}

die() {
  printf 'Error: %s\n' "$*" >&2
  exit 1
}

abs_path() {
  local path="$1"
  local dir
  local base

  dir="$(dirname "$path")" || return 1
  base="$(basename "$path")" || return 1
  (cd "$dir" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$base")
}

find_patch_dirs() {
  local pattern="$1"

  find "$WORK_ROOT" \
    \( -type d \( -name .git -o -name vendor \) \) -prune \
    -o -type d -name "$pattern" -print | sort
}

latest_patch_version() {
  local versions

  versions="$(find_patch_dirs 'patches-*' | sed 's#.*/patches-##')"
  [ -n "$versions" ] || return 1

  if command -v php >/dev/null 2>&1; then
    printf '%s\n' "$versions" | php -r '
      $versions = array_values(array_filter(array_map("trim", file("php://stdin"))));
      usort($versions, "version_compare");
      echo end($versions), PHP_EOL;
    '
  else
    printf '%s\n' "$versions" | sort | tail -n 1
  fi
}

patch_files_in_dir() {
  local patch_dir="$1"

  [ -d "$patch_dir" ] || return 0

  find "$patch_dir" -type f \
    \( -name '*.patch' -o -name '*.diff' -o -name '*.patches' \) -print | sort
}

strip_level_for_patch() {
  local patch_file="$1"
  local strip_level

  strip_level="$(
    awk '
      /^(---|\+\+\+) / {
        path = $2
        if (path == "/dev/null") {
          next
        }
        gsub(/^"|"$/, "", path)
        parts = split(path, component, "/")
        for (i = 1; i <= parts; i++) {
          if (component[i] == "CRM" || component[i] == "templates") {
            print i - 1
            exit
          }
        }
      }
    ' "$patch_file"
  )"

  if [ -n "$strip_level" ]; then
    printf '%s\n' "$strip_level"
  else
    printf '1\n'
  fi
}

prepare_checkout() {
  local checkout_dir="$1"

  mkdir -p "$TMP_ROOT" || return 1

  if [ -d "$checkout_dir/.git" ]; then
    log "Updating $checkout_dir from $REPO_URL ($VERSION) ..."
    git -C "$checkout_dir" remote set-url origin "$REPO_URL" || return 1
    git -C "$checkout_dir" fetch --quiet --depth 1 origin "$VERSION" || \
      git -C "$checkout_dir" fetch --quiet origin "$VERSION" || return 1
    git -C "$checkout_dir" checkout --quiet -B "cividesk-patch-$VERSION" FETCH_HEAD || return 1
  else
    [ ! -e "$checkout_dir" ] || die "$checkout_dir exists but is not a git checkout"
    log "Cloning $REPO_URL ($VERSION) into $checkout_dir ..."
    git clone --quiet --depth 1 --branch "$VERSION" "$REPO_URL" "$checkout_dir" || return 1
  fi

  reset_checkout "$checkout_dir"
}

reset_checkout() {
  local checkout_dir="$1"
  local dirty_paths

  git -C "$checkout_dir" reset --hard --quiet HEAD || return 1
  git -C "$checkout_dir" clean -ffdx --quiet || return 1

  dirty_paths="$(git -C "$checkout_dir" status --porcelain --untracked-files=all --ignored)" || return 1
  if [ -n "$dirty_paths" ]; then
    warn "$checkout_dir is not clean after reset:"
    printf '%s\n' "$dirty_paths" >&2
    return 1
  fi
}

reject_files() {
  local checkout_dir="$1"

  find "$checkout_dir" \
    \( -path "$checkout_dir/.git" -o -path "$checkout_dir/vendor" \) -prune \
    -o -type f -name '*.rej' -print | sort
}

clear_reject_files() {
  local checkout_dir="$1"
  local reject_file

  while IFS= read -r reject_file; do
    [ -n "$reject_file" ] || continue
    rm -f "$reject_file" || return 1
  done <<EOF
$(reject_files "$checkout_dir")
EOF
}

append_rejected_chunks() {
  local checkout_dir="$1"
  local rejects
  local reject_file
  local relative_path

  rejects="$(reject_files "$checkout_dir")"
  if [ -z "$rejects" ]; then
    printf '    Rejected chunks: none found\n' >> "$FAILURE_REPORT"
    return 0
  fi

  printf '    Rejected chunks:\n' >> "$FAILURE_REPORT"
  while IFS= read -r reject_file; do
    [ -n "$reject_file" ] || continue
    relative_path="${reject_file#"$checkout_dir"/}"
    printf '      %s\n' "$relative_path" >> "$FAILURE_REPORT"
    sed 's/^/        /' "$reject_file" >> "$FAILURE_REPORT"
  done <<EOF
$rejects
EOF
}

check_patch_file() {
  local checkout_dir="$1"
  local patch_file="$2"
  local strip_level
  local apply_output

  strip_level="$(strip_level_for_patch "$patch_file")"
  patch_file="$(abs_path "$patch_file")"

  apply_output="$(git -C "$checkout_dir" apply --reject --whitespace=nowarn -p"$strip_level" "$patch_file" 2>&1)"
  if [ "$?" -eq 0 ]; then
    printf '%s\n' "$patch_file" >> "$SUCCESS_LIST"
    return 0
  fi

  printf '  %s\n' "$patch_file" >> "$FAILURE_REPORT"
  if [ -n "$apply_output" ]; then
    printf '    git apply output:\n' >> "$FAILURE_REPORT"
    printf '%s\n' "$apply_output" | sed 's/^/      /' >> "$FAILURE_REPORT"
  fi
  append_rejected_chunks "$checkout_dir"
  clear_reject_files "$checkout_dir"

  return 1
}

print_summary() {
  log ""
  log "Successful patches:"
  if [ -s "$SUCCESS_LIST" ]; then
    sed 's/^/  /' "$SUCCESS_LIST"
  else
    log "  (none)"
  fi

  log ""
  log "Unsuccessful patches:"
  if [ -s "$FAILURE_REPORT" ]; then
    cat "$FAILURE_REPORT"
  else
    log "  (none)"
  fi
}

cleanup() {
  [ -n "$SUCCESS_LIST" ] && rm -f "$SUCCESS_LIST"
  [ -n "$FAILURE_REPORT" ] && rm -f "$FAILURE_REPORT"
}

parse_args() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      -v|--version)
        [ "$#" -ge 2 ] || die "$1 requires a value"
        VERSION="$2"
        shift 2
        ;;
      -t|--tmp-dir)
        [ "$#" -ge 2 ] || die "$1 requires a value"
        TMP_ROOT="$2"
        shift 2
        ;;
      --repo-url)
        [ "$#" -ge 2 ] || die "$1 requires a value"
        REPO_URL="$2"
        shift 2
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "Unknown option: $1"
        ;;
    esac
  done
}

main() {
  local checkout_dir
  local patch_dirs
  local patch_dir
  local patch_files
  local patch_file
  local had_failure=0

  parse_args "$@"

  command -v git >/dev/null 2>&1 || die "git is required"
  command -v sed >/dev/null 2>&1 || die "sed is required"
  command -v awk >/dev/null 2>&1 || die "awk is required"

  SUCCESS_LIST="$(mktemp "${TMPDIR:-/tmp}/cividesk-patch-status-success.XXXXXX")" || die "Could not create temporary file"
  FAILURE_REPORT="$(mktemp "${TMPDIR:-/tmp}/cividesk-patch-status-failure.XXXXXX")" || die "Could not create temporary file"
  trap cleanup EXIT

  if [ -z "$VERSION" ]; then
    VERSION="$(latest_patch_version)" || die "No patches-[version] directories found"
  fi

  patch_dirs="$(find_patch_dirs "patches-$VERSION")"
  [ -n "$patch_dirs" ] || die "No patches-$VERSION directories found under $WORK_ROOT"

  checkout_dir="$TMP_ROOT/civicrm-core-$VERSION"
  prepare_checkout "$checkout_dir" || die "Could not prepare $checkout_dir"

  while IFS= read -r patch_dir; do
    [ -n "$patch_dir" ] || continue

    patch_files="$(patch_files_in_dir "$patch_dir")"
    if [ -z "$patch_files" ]; then
      warn "No patch files found in $patch_dir"
      continue
    fi

    log "Checking $patch_dir"
    reset_checkout "$checkout_dir" || die "Could not reset $checkout_dir"

    while IFS= read -r patch_file; do
      [ -n "$patch_file" ] || continue
      if check_patch_file "$checkout_dir" "$patch_file"; then
        log "  OK $(basename "$patch_file")"
      else
        warn "Patch failed: $patch_file"
        had_failure=1
      fi
    done <<EOF
$patch_files
EOF
  done <<EOF
$patch_dirs
EOF

  print_summary

  if [ "$had_failure" -ne 0 ]; then
    exit 1
  fi
}

main "$@"
