#!/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=""

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

Apply CiviCRM core patches from patches-[version] directories and copy the
changed CRM/ and templates/ files back beside each patch directory.

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
}

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

  strip_level="$(strip_level_for_patch "$patch_file")"
  log "  Applying $(basename "$patch_file") (-p$strip_level)"

  git -C "$checkout_dir" apply --reject --whitespace=nowarn -p"$strip_level" "$patch_file"
}

changed_core_paths() {
  local checkout_dir="$1"
  local path

  git -C "$checkout_dir" status --porcelain --untracked-files=all -- CRM templates |
    sed -E 's/^.. //; s/^.* -> //' |
    while IFS= read -r path; do
      [ -n "$path" ] || continue

      printf '%s\n' "$path"
      case "$path" in
        *.rej)
          printf '%s\n' "${path%.rej}"
          ;;
      esac
    done | sort -u
}

copy_changed_core_files() {
  local checkout_dir="$1"
  local destination_root="$2"
  local copied=0
  local skipped=0
  local path
  local source
  local destination

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

    source="$checkout_dir/$path"
    destination="$destination_root/$path"

    if [ ! -e "$source" ]; then
      warn "Skipping deleted file $path"
      skipped=$((skipped + 1))
      continue
    fi

    mkdir -p "$(dirname "$destination")" || return 1
    cp -p "$source" "$destination" || return 1
    copied=$((copied + 1))
  done <<EOF
$(changed_core_paths "$checkout_dir")
EOF

  log "  Copied $copied core file(s) and reject artifact(s) to $destination_root"
  if [ "$skipped" -gt 0 ]; then
    warn "$skipped deleted file(s) were not copied"
  fi
}

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 destination_root
  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"

  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

    destination_root="$(dirname "$(abs_path "$patch_dir")")"
    log "Processing $patch_dir"

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

    reset_checkout "$checkout_dir" || die "Could not reset $checkout_dir"

    while IFS= read -r patch_file; do
      [ -n "$patch_file" ] || continue
      patch_file="$(abs_path "$patch_file")"
      if ! apply_patch_file "$checkout_dir" "$patch_file"; then
        warn "Patch failed: $patch_file"
        had_failure=1
      fi
    done <<EOF
$patch_files
EOF

    copy_changed_core_files "$checkout_dir" "$destination_root" || die "Could not copy changed files"
  done <<EOF
$patch_dirs
EOF

  if [ "$had_failure" -ne 0 ]; then
    warn "One or more patches failed; copied any partial changes and reject artifacts that were produced."
    exit 1
  fi
}

main "$@"
