#!/usr/bin/env bash
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

set -Eeuo pipefail

CURRENT_STEP=""
VERIFY_SUCCESS="no"
VERIFY_OWNS_TMPDIR="0"
VERIFY_TMPDIR=""

on_error() {
  local status=$?
  if [ -n "${CURRENT_STEP}" ]; then
    echo "FAILED: ${CURRENT_STEP}" >&2
  else
    echo "FAILED" >&2
  fi
  exit "${status}"
}

cleanup() {
  if [ -z "${VERIFY_TMPDIR}" ]; then
    return
  fi

  if [ "${VERIFY_SUCCESS}" = "yes" ]; then
    if [ "${VERIFY_OWNS_TMPDIR}" = "1" ]; then
      rm -rf "${VERIFY_TMPDIR}"
    fi
  else
    echo "Failed to verify release candidate. See ${VERIFY_TMPDIR} for details." >&2
  fi
}

trap on_error ERR
trap cleanup EXIT

usage() {
  cat <<USAGE
Usage:
  $0 <version> <rc> [options]

Arguments:
  <version>
      Release version in <major>.<minor>.<patch> format.
      Example: 0.9.1

  <rc>
      Numeric release candidate round.
      Example: 2 verifies apache-iceberg-rust-0.9.1-rc2.

Options:
  --dist_dir <dir>
      Local artifact root used when --download is 0.
      Relative paths are resolved from the repository root.
      Default: dist

  --download <0|1>
      Whether to download artifacts from ASF dev dist. Use 0 to verify local artifacts.
      Default: 1

  --verify_signature <0|1>
      Whether to verify the .asc signature with the local GPG keyring.
      Default: 1

  --import_gpg_keys <0|1>
      Whether to download and import Apache Iceberg release keys before signature verification.
      Default: 0

  --verify_git_tag <0|1>
      Whether to verify that the source archive matches the git tag v<version>-rc.<rc>.
      The tag is cloned and re-archived, then compared against the extracted archive.
      Default: 1

  --git_repo_url <url>
      Git repository URL used to clone the RC tag for the git tag comparison.
      Default: https://github.com/apache/iceberg-rust.git

  --check_headers <0|1>
      Whether to check Apache license headers against the extracted source archive.
      Default: 1

  --build <0|1>
      Whether to build and test the Rust source distribution.
      Default: 1

  --python <0|1>
      Whether to build and test pyiceberg-core.
      Default: 1

  --tmp_dir <dir>
      Verification sandbox. If omitted, a temporary directory is created and deleted on success.
      Default: auto-created temporary directory

  -h, --help
      Show this help message.

Examples:
  $0 0.9.1 2
  $0 0.9.1 2 --download 0
  $0 0.9.1 2 --import_gpg_keys 1
  $0 0.9.1 2 --verify_git_tag 0
  $0 0.9.1 2 --download 0 --build 0 --python 0 --check_headers 0 --verify_signature 0
USAGE
}

show_help_if_requested() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      -h | --help)
        usage
        exit 0
        ;;
    esac
    shift
  done
}

start_step() {
  CURRENT_STEP="$1"
  echo "==> ${CURRENT_STEP}"
}

finish_step() {
  echo "OK: ${CURRENT_STEP}"
  CURRENT_STEP=""
}

run_step() {
  local step="$1"
  shift
  start_step "${step}"
  "$@"
  finish_step
}

skip_step() {
  local step="$1"
  start_step "${step}"
  echo "OK: ${CURRENT_STEP} (skipped)"
  CURRENT_STEP=""
}

enabled() {
  [ "$1" != "0" ]
}

require_command() {
  local command_name="$1"
  if ! command -v "${command_name}" >/dev/null 2>&1; then
    echo "This step requires '${command_name}', but it is not installed." >&2
    return 1
  fi
}

require_checksum_command() {
  if ! command -v shasum >/dev/null 2>&1 && ! command -v sha512sum >/dev/null 2>&1; then
    echo "This step requires either 'shasum' or 'sha512sum'." >&2
    return 1
  fi
}

require_option_value() {
  local option_name="$1"
  local option_value="${2:-}"
  if [ -z "${option_value}" ]; then
    echo "Missing value for ${option_name}." >&2
    usage >&2
    return 1
  fi
}

validate_bool_option() {
  local option_name="$1"
  local option_value="$2"
  case "${option_value}" in
    0 | 1) ;;
    *)
      echo "${option_name} must be 0 or 1, got '${option_value}'." >&2
      return 1
      ;;
  esac
}

parse_args() {
  local positional=()

  while [ "$#" -gt 0 ]; do
    case "$1" in
      -h | --help)
        usage
        exit 0
        ;;
      --dist_dir | --dist-dir)
        require_option_value "$1" "${2:-}"
        RELEASE_DIST_DIR="$2"
        shift 2
        ;;
      --download)
        require_option_value "$1" "${2:-}"
        validate_bool_option "$1" "$2"
        DOWNLOAD="$2"
        shift 2
        ;;
      --verify_signature | --verify-signature)
        require_option_value "$1" "${2:-}"
        validate_bool_option "$1" "$2"
        VERIFY_SIGNATURE="$2"
        shift 2
        ;;
      --import_gpg_keys | --import-gpg-keys)
        require_option_value "$1" "${2:-}"
        validate_bool_option "$1" "$2"
        IMPORT_GPG_KEYS="$2"
        shift 2
        ;;
      --verify_git_tag | --verify-git-tag)
        require_option_value "$1" "${2:-}"
        validate_bool_option "$1" "$2"
        VERIFY_GIT_TAG="$2"
        shift 2
        ;;
      --git_repo_url | --git-repo-url)
        require_option_value "$1" "${2:-}"
        GIT_REPO_URL="$2"
        shift 2
        ;;
      --check_headers | --check-headers)
        require_option_value "$1" "${2:-}"
        validate_bool_option "$1" "$2"
        CHECK_HEADERS="$2"
        shift 2
        ;;
      --build)
        require_option_value "$1" "${2:-}"
        validate_bool_option "$1" "$2"
        BUILD="$2"
        shift 2
        ;;
      --python)
        require_option_value "$1" "${2:-}"
        validate_bool_option "$1" "$2"
        PYTHON="$2"
        shift 2
        ;;
      --tmp_dir | --tmp-dir)
        require_option_value "$1" "${2:-}"
        VERIFY_TMPDIR="$2"
        shift 2
        ;;
      --*)
        echo "Unknown option: $1" >&2
        usage >&2
        return 1
        ;;
      *)
        positional+=("$1")
        shift
        ;;
    esac
  done

  if [ "${#positional[@]}" -ne 2 ]; then
    usage >&2
    return 1
  fi

  VERSION="${positional[0]}"
  RC="${positional[1]}"
}

validate_args() {
  if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "Version '${VERSION}' must use format <major>.<minor>.<patch>." >&2
    return 1
  fi

  if [[ ! "${RC}" =~ ^[0-9]+$ ]]; then
    echo "RC '${RC}' must be a number, for example: 0, 1, or 2." >&2
    return 1
  fi
}

derive_release_names() {
  RC_TAG="v${VERSION}-rc.${RC}"
  RC_ID="apache-iceberg-rust-${VERSION}-rc${RC}"
  ARCHIVE_BASE_NAME="apache-iceberg-rust-${VERSION}"
  ARCHIVE_FILE_NAME="${ARCHIVE_BASE_NAME}.tar.gz"
  ICEBERG_DIST_BASE_URL="https://downloads.apache.org/iceberg"
  DOWNLOAD_RC_BASE_URL="https://dist.apache.org/repos/dist/dev/iceberg/${RC_ID}"

  if [[ "${RELEASE_DIST_DIR}" = /* ]]; then
    LOCAL_DIST_ROOT="${RELEASE_DIST_DIR}"
  else
    LOCAL_DIST_ROOT="${REPO_ROOT}/${RELEASE_DIST_DIR}"
  fi
  LOCAL_RC_DIR="${LOCAL_DIST_ROOT}/${RC_ID}"
}

setup_tmpdir() {
  if [ -z "${VERIFY_TMPDIR}" ]; then
    require_command mktemp
    VERIFY_TMPDIR="$(mktemp -d -t "iceberg-rust-${VERSION}-rc${RC}.XXXXXX")"
    VERIFY_OWNS_TMPDIR="1"
  else
    mkdir -p "${VERIFY_TMPDIR}"
  fi
}

enter_tmpdir() {
  cd "${VERIFY_TMPDIR}"
}

download_file() {
  require_command curl
  local file_name="$1"
  curl \
    --fail \
    --location \
    --remote-name \
    --show-error \
    --silent \
    "${DOWNLOAD_RC_BASE_URL}/${file_name}"
}

copy_local_file() {
  require_command cp
  local file_name="$1"
  cp "${LOCAL_RC_DIR}/${file_name}" "${file_name}"
}

fetch_file() {
  local file_name="$1"
  if enabled "${DOWNLOAD}"; then
    download_file "${file_name}"
  else
    copy_local_file "${file_name}"
  fi
}

import_gpg_keys() {
  require_command curl
  require_command gpg
  curl \
    --fail \
    --location \
    --output KEYS \
    --show-error \
    --silent \
    "${ICEBERG_DIST_BASE_URL}/KEYS"
  gpg --import KEYS
}

verify_source_signature() {
  require_command gpg
  gpg --verify "${ARCHIVE_FILE_NAME}.asc" "${ARCHIVE_FILE_NAME}"
}

verify_checksum() {
  require_checksum_command
  if command -v shasum >/dev/null 2>&1; then
    shasum -a 512 -c "${ARCHIVE_FILE_NAME}.sha512"
  else
    sha512sum -c "${ARCHIVE_FILE_NAME}.sha512"
  fi
}

extract_source_archive() {
  require_command tar
  tar -xzf "${ARCHIVE_FILE_NAME}"
}

verify_git_tag_matches_archive() {
  require_command git
  require_command tar
  require_command diff

  local tag_checkout_dir="git-tag-checkout"
  local tag_archive_dir="git-tag-archive"
  local tag_archive_file="git-tag-source.tar.gz"

  # Clone only the RC tag from the git repository.
  git clone --quiet --depth 1 --branch "${RC_TAG}" "${GIT_REPO_URL}" "${tag_checkout_dir}"

  # Recreate the source archive from the tag using the same layout as create_rc.sh
  # (git archive honors .gitattributes export-ignore rules, e.g. the website dir).
  git -C "${tag_checkout_dir}" archive \
    --format=tar.gz \
    --prefix="${ARCHIVE_BASE_NAME}/" \
    --output="${VERIFY_TMPDIR}/${tag_archive_file}" \
    HEAD

  mkdir -p "${tag_archive_dir}"
  tar -xzf "${tag_archive_file}" -C "${tag_archive_dir}"

  # Fail if the released archive differs from what the git tag produces.
  if ! diff -r "${tag_archive_dir}/${ARCHIVE_BASE_NAME}" "${ARCHIVE_BASE_NAME}"; then
    echo "Source archive does not match git tag ${RC_TAG}." >&2
    return 1
  fi
}

check_license_headers() {
  require_command docker
  docker run --rm -v "${VERIFY_TMPDIR}/${ARCHIVE_BASE_NAME}:/github/workspace" apache/skywalking-eyes header check
}

test_source_distribution() {
  require_command make
  (
    trap - ERR
    cd "${ARCHIVE_BASE_NAME}"
    make build
    make test
  )
}

test_python_distribution() {
  require_command make
  require_command uv
  (
    trap - ERR
    cd "${ARCHIVE_BASE_NAME}/bindings/python"
    make install
    make test
  )
}

mark_success() {
  VERIFY_SUCCESS="yes"
  echo "RC looks good!"
}

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"

VERSION=""
RC=""
RELEASE_DIST_DIR="dist"
DOWNLOAD="1"
VERIFY_SIGNATURE="1"
IMPORT_GPG_KEYS="0"
VERIFY_GIT_TAG="1"
GIT_REPO_URL="https://github.com/apache/iceberg-rust.git"
CHECK_HEADERS="1"
BUILD="1"
PYTHON="1"

show_help_if_requested "$@"
run_step "Parse command arguments" parse_args "$@"
run_step "Validate release arguments" validate_args
derive_release_names

run_step "Create verification sandbox" setup_tmpdir
run_step "Enter verification sandbox ${VERIFY_TMPDIR}" enter_tmpdir
run_step "Fetch source archive ${ARCHIVE_FILE_NAME}" fetch_file "${ARCHIVE_FILE_NAME}"

if enabled "${VERIFY_SIGNATURE}"; then
  run_step "Fetch source archive signature" fetch_file "${ARCHIVE_FILE_NAME}.asc"
  if enabled "${IMPORT_GPG_KEYS}"; then
    run_step "Import Apache Iceberg GPG keys" import_gpg_keys
  else
    skip_step "Import Apache Iceberg GPG keys"
  fi
  run_step "Verify source archive signature" verify_source_signature
else
  skip_step "Fetch source archive signature"
  skip_step "Import Apache Iceberg GPG keys"
  skip_step "Verify source archive signature"
fi

run_step "Fetch source archive checksum" fetch_file "${ARCHIVE_FILE_NAME}.sha512"
run_step "Verify source archive checksum" verify_checksum
run_step "Extract source archive" extract_source_archive

if enabled "${VERIFY_GIT_TAG}"; then
  run_step "Verify source archive matches git tag ${RC_TAG}" verify_git_tag_matches_archive
else
  skip_step "Verify source archive matches git tag ${RC_TAG}"
fi

if enabled "${CHECK_HEADERS}"; then
  run_step "Check license headers" check_license_headers
else
  skip_step "Check license headers"
fi

if enabled "${BUILD}"; then
  run_step "Build and test Rust source distribution" test_source_distribution
else
  skip_step "Build and test Rust source distribution"
fi

if enabled "${PYTHON}"; then
  run_step "Build and test pyiceberg-core" test_python_distribution
else
  skip_step "Build and test pyiceberg-core"
fi

run_step "Print verification summary" mark_success
