# Constants
BOLD_PINK='\e[35;1m'
RESET='\e[0m'
GREEN='\e[32;1m'
RED='\e[31;1m'
BLUE='\e[34;1m'
YELLOW='\e[33;1m'
UNAME_S=$(uname -s)

function LOG_INFO() {
  printf "${GREEN}$1${RESET}\n"
}

function LOG_ERROR() {
  printf "${RED}$1${RESET}\n"
}

function LOG_NOTICE() {
  printf "${BOLD_PINK}$1${RESET}\n"
}

function LOG_WARNING() {
  printf "${YELLOW}$1${RESET}\n"
}

function print_environment_var() {
  local varname=$1
  local value=$2
  printf "${BLUE}${varname}${RESET} => ${GREEN}${value}${RESET}\n"
}

if [[ "${UNAME_S}" == "Darwin" ]]; then
  export MODULE_EXT="dylib"
else
  export MODULE_EXT="so"
fi

function num_proc() {
  if [[ "${UNAME_S}" == "Darwin" ]]; then
    echo $(sysctl -n hw.ncpu)
  else
    echo $(nproc)
  fi
}

function findup() {
  # Check if a filename was provided
  if [ -z "$1" ]; then
    echo "Usage: findup filename"
    exit 1
  fi

  # Start from current directory
  local path="$(pwd)"

  # Continue until we reach root directory
  while [[ "$path" != "/" ]]; do
    # Check if file exists in current path
    if [ -f "$path/$1" ]; then
      echo "$path"
      return
    fi
    # Move up to parent directory
    path="$(dirname "$path")"
  done
  exit 1
}

# Determine pip command
PYTHON_PATH="${PYTHON_PATH:=}"
_WORKSPACE_HOME=$(findup QUICK_START.md)
export MODULE_ROOT=${_WORKSPACE_HOME}
_THIRD_PARTY_COMPONENTS_BUILD_DIR=${_WORKSPACE_HOME}/.build-release

if [[ "${SAN_BUILD}" != "no" ]]; then
  if [[ "$(uname -s)" == "Darwin" ]]; then
    # detect_leaks is not support on macOS
    export ASAN_OPTIONS="detect_odr_violation=0:halt_on_error=1"
  else
    export ASAN_OPTIONS="detect_odr_violation=0:detect_leaks=1:halt_on_error=1"
  fi
  if [[ "${SAN_BUILD}" == "address" ]]; then
    export LSAN_OPTIONS="suppressions=${MODULE_ROOT}/ci/asan.supp"
    print_environment_var "LSAN_OPTIONS" "${LSAN_OPTIONS}"
  else
    # TSAN deadlock detector overflows with valkey's lock count during module load.
    export TSAN_OPTIONS="detect_deadlocks=0:suppressions=${MODULE_ROOT}/ci/tsan.supp"
    print_environment_var "TSAN_OPTIONS" "${TSAN_OPTIONS}"
  fi
  print_environment_var "ASAN_OPTIONS" "${ASAN_OPTIONS}"
fi

function get_third_party_build_dir() {
  if [ -z "${SAN_BUILD}" ] || [[ "${SAN_BUILD}" == "no" ]]; then
    echo ${_THIRD_PARTY_COMPONENTS_BUILD_DIR}
  elif [[ "${SAN_BUILD}" == "thread" ]]; then
    echo ${_THIRD_PARTY_COMPONENTS_BUILD_DIR}-tsan
  else
    echo ${_THIRD_PARTY_COMPONENTS_BUILD_DIR}-asan
  fi
}

# Capitalize a word. This method is compatible with bash-3 and bash-4
function capitalize_string() {
  local string=$1
  local first_char=${string:0:1}
  local remainder=${string:1}
  first_char=$(echo "${first_char}" | tr '[:lower:]' '[:upper:]')
  remainder=$(echo "${remainder}" | tr '[:upper:]' '[:lower:]')
  echo ${first_char}${remainder}
}

function pip_check_if_module_installed() {
  local module_name=$1
  set +e
  ${PIP_PATH} show ${module_name} >/dev/null 2>&1
  RESULT=$?
  set -e
  if [ ${RESULT} -eq 0 ]; then
    echo "yes"
  else
    echo "no"
  fi
}

function install_test_framework() {
  if [ ! -z "${VALKEY_TEST_FRAMEWORK}" ] &&
    [ -f "${VALKEY_TEST_FRAMEWORK}/LICENSE" ]; then
    LOG_INFO "Using valkey-test-framework => ${VALKEY_TEST_FRAMEWORK}"
    return
  fi

  local test_framework_url="https://github.com/valkey-io/valkey-test-framework"
  local test_framework_path="${_WORKSPACE_HOME}/integration/valkeytestframework.git"

  if [ ! -f ${test_framework_path}/LICENSE ]; then
    rm -fr ${test_framework_path}
    LOG_INFO "Cloning valkey-test-framework into ${test_framework_path}"
    git clone "${test_framework_url}" ${test_framework_path}
  fi

  pushd ${test_framework_path} >/dev/null
  valkey_module_installed=$(pip_check_if_module_installed valkey)
  if [[ "${valkey_module_installed}" == "no" ]]; then
    LOG_INFO "Installing dependencies"
    local requirements_txt=${test_framework_path}/requirements.txt
    local testing_requirements_txt=${_WORKSPACE_HOME}/testing/integration/requirements.txt
    if [ -f ${requirements_txt} ]; then
      LOG_INFO "Installing requirements file"
      ${PIP_PATH} install -r ${requirements_txt} -r ${testing_requirements_txt}
      ${PIP_PATH} install --upgrade pytest
    else
      LOG_ERROR "Missing ${requirements_txt} file. Broken installation?"
    fi
  fi
  popd >/dev/null

  # Remove valkey-test-framework tests
  rm -fr ${_WORKSPACE_HOME}/integration/valkeytestframework.git/tests
  ln -sf ${_WORKSPACE_HOME}/integration/valkeytestframework.git/src ${_WORKSPACE_HOME}/integration/valkeytestframework
  LOG_INFO "Using valkeytestframework => ${_WORKSPACE_HOME}/integration/valkeytestframework"
  print_environment_var "PYTHONPATH" "${PYTHONPATH}"
  export PYTHONPATH=${_WORKSPACE_HOME}/integration/valkeytestframework
}

function setup_valkey_server() {
  LOG_INFO "Building valkey-server...(SAN_BUILD=${SAN_BUILD})"
  if [ ! -z "${VALKEY_SERVER_PATH}" ] && [ -f "${VALKEY_SERVER_PATH}" ]; then
    LOG_INFO "Using valkey-server => ${VALKEY_SERVER_PATH}"
    return
  fi

  # Clone and build it
  VALKEY_VERSION="${VALKEY_VERSION:=9.0.1}"
  export VALKEY_SERVER_HOME_DIR=$(get_third_party_build_dir)/valkey-server
  export VALKEY_SERVER_BUILD_DIR=${VALKEY_SERVER_HOME_DIR}/.build-release
  if [ ! -d ${VALKEY_SERVER_HOME_DIR} ]; then
    LOG_INFO "Cloning valkey-server@${VALKEY_VERSION} into ${VALKEY_SERVER_HOME_DIR}"
    git clone --branch ${VALKEY_VERSION} --single-branch https://github.com/valkey-io/valkey.git ${VALKEY_SERVER_HOME_DIR}
  fi

  mkdir -p ${VALKEY_SERVER_BUILD_DIR}
  pushd ${VALKEY_SERVER_BUILD_DIR} >/dev/null

  if [ -f "${VALKEY_SERVER_BUILD_DIR}/bin/valkey-server" ]; then
    export VALKEY_SERVER_PATH=${VALKEY_SERVER_BUILD_DIR}/bin/valkey-server
    export VALKEY_CLI_PATH=${VALKEY_SERVER_BUILD_DIR}/bin/valkey-cli
    LOG_INFO "Found valkey-server: ${VALKEY_SERVER_PATH}"
    LOG_INFO "Found valkey-cli: ${VALKEY_CLI_PATH}"
    return
  fi

  if [ ! -z "${SAN_BUILD}" ] && [[ "${SAN_BUILD}" != "no" ]]; then
    VALKEY_CMAKE_EXTRA_ARGS="-DBUILD_SANITIZER=${SAN_BUILD}"
    LOG_INFO "Building Valkey with Sanitizer options: -DBUILD_SANITIZER=${SAN_BUILD}"
    # Temporary workaround until these are merged into core upstream:
    # 1. IFUNC resolvers run before TSAN runtime initializes, so no_sanitize("thread") is required.
    # https://github.com/valkey-io/valkey/blob/4dee322edeb9c96029d89cfaf1600abad2afead0/src/util.c#L640-L642
    sed -i 's/__attribute__((no_sanitize_address, used)) static int (\*string2ll_resolver/__attribute__((no_sanitize_address, no_sanitize("thread"), used)) static int (*string2ll_resolver/' \
      "${VALKEY_SERVER_HOME_DIR}/src/util.c"

    # 2. RTLD_DEEPBIND is incompatible with TSAN; exclude it like ASAN already does.
    # https://github.com/valkey-io/valkey/blame/ab3c953b80289d88991095f53c1235fc2f8b44d6/src/module.c#L12548
    sed -i 's/!defined(VALKEY_ADDRESS_SANITIZER) \&\& __has_include(<dlfcn.h>)/!defined(VALKEY_ADDRESS_SANITIZER) \&\& !defined(VALKEY_THREAD_SANITIZER) \&\& __has_include(<dlfcn.h>)/' \
      "${VALKEY_SERVER_HOME_DIR}/src/module.c"

    # 3. Define VALKEY_THREAD_SANITIZER via __SANITIZE_THREAD__ (mirrors VALKEY_ADDRESS_SANITIZER pattern).
    # https://github.com/valkey-io/valkey/blob/4dee322edeb9c96029d89cfaf1600abad2afead0/src/config.h#L175-L181
    grep -q 'VALKEY_THREAD_SANITIZER' "${VALKEY_SERVER_HOME_DIR}/src/config.h" || \
      sed -i '/^#if defined(__SANITIZE_ADDRESS__)/i #if defined(__SANITIZE_THREAD__)\n#define VALKEY_THREAD_SANITIZER 1\n#endif\n' \
        "${VALKEY_SERVER_HOME_DIR}/src/config.h"
  fi

  LOG_INFO "Working directory: ${VALKEY_SERVER_BUILD_DIR}"
  LOG_INFO "Building valkey-server: cmake -DCMAKE_BUILD_TYPE=Release .. ${VALKEY_CMAKE_EXTRA_ARGS}"
  cmake -DCMAKE_BUILD_TYPE=Release .. ${VALKEY_CMAKE_EXTRA_ARGS}
  make -j$(num_proc)
  popd >/dev/null
  export VALKEY_SERVER_PATH=${VALKEY_SERVER_BUILD_DIR}/bin/valkey-server
  export VALKEY_CLI_PATH=${VALKEY_SERVER_BUILD_DIR}/bin/valkey-cli
  LOG_INFO "VALKEY_SERVER_PATH => ${VALKEY_SERVER_PATH}"
  LOG_INFO "Found valkey-cli: ${VALKEY_CLI_PATH}"
}

function setup_json_module() {
  LOG_INFO "Building valkey-json..."
  if [ ! -z "${VALKEY_JSON_PATH}" ] && [ -f "${VALKEY_JSON_PATH}" ]; then
    LOG_INFO "Using valkey-json => ${VALKEY_JSON_PATH}"
    return
  fi

  # Clone and build it
  VALKEY_JSON_VERSION="${VALKEY_JSON_VERSION:=1.0}"
  export VALKEY_JSON_HOME=$(get_third_party_build_dir)/valkey-json
  export VALKEY_JSON_BUILD_DIR=${VALKEY_JSON_HOME}/.build-release

  local json_module_path=${VALKEY_JSON_HOME}/build/src/libjson.${MODULE_EXT}
  if [ -f "${json_module_path}" ]; then
    export VALKEY_JSON_PATH="${json_module_path}"
    LOG_INFO "Found valkey-json: ${VALKEY_JSON_PATH}"
    return
  fi

  rm -fr "${VALKEY_JSON_HOME}"
  LOG_INFO "Cloning valkey-json@${VALKEY_JSON_VERSION} into ${VALKEY_JSON_HOME}"
  git clone --branch ${VALKEY_JSON_VERSION} --single-branch https://github.com/valkey-io/valkey-json.git ${VALKEY_JSON_HOME}

  pushd ${VALKEY_JSON_HOME} >/dev/null
  LOG_INFO "Working directory: ${VALKEY_JSON_HOME}"
  LOG_INFO "Building valkey-json: ./build.sh --release"
  ./build.sh --release
  popd >/dev/null

  export VALKEY_JSON_PATH=${json_module_path}
  LOG_INFO "VALKEY_JSON_PATH => ${VALKEY_JSON_PATH}"
}

# Loop over Valkey log files and search for sanitization errors
function check_for_san_errors() {
  local valkey_logs="$1"
  local exit_with_error=0
  local files_to_dump=""
  for logfile in ${valkey_logs}; do
    printf "Checking log file ${logfile} for ASan/TSan errors"
    local errors_count=$(cat ${logfile} | grep -wE 'AddressSanitizer|ThreadSanitizer|LeakSanitizer' | wc -l)
    if [[ ${errors_count} -eq 0 ]]; then
      printf "... ${GREEN}ok${RESET}\n"
    else
      printf "... ${RED}found errors!${RESET}\n"
      exit_with_error=1
      files_to_dump="${files_to_dump} ${logfile}"
    fi
  done

  if [[ ${exit_with_error} -ne 0 ]]; then
    printf "\n\nDumping log files with errors\n\n"
    for file in ${files_to_dump}; do
      cat $file
      printf "\n\n -------------------------------- \n\n"
    done
    exit 1
  fi
}

function get_file_last_modified() {
    echo $(date -r "${1}" +%s)
}

function run_with_timeout() {
    local timeout=$1
    shift  # Remove the first argument (timeout), rest is the command
    
    # Validate timeout is a number
    if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
        echo "Error: Timeout must be a positive integer" >&2
        return 1
    fi
    
    # Start the command in background
    echo "Starting test in run_with_timeout"
    "$@" &
    local pid=$!
    
    # Start a timer in background
    (
        sleep "$timeout"
        # Check if process is still running
        if kill -0 "$pid" 2>/dev/null; then
            echo "Process $pid exceeded timeout of ${timeout}s, terminating..." >&2
            kill -TERM "$pid" 2>/dev/null
            sleep 2
            # Force kill if still running
            if kill -0 "$pid" 2>/dev/null; then
                echo "Process $pid did not respond to TERM, sending KILL..." >&2
                kill -KILL "$pid" 2>/dev/null
            fi
        fi
    ) &
    local timer_pid=$!
    
    # Wait for the command to complete
    wait "$pid" 2>/dev/null
    local exit_code=$?
    echo "Finish test in run_with_timeout ${exit_code}"
    # Kill the timer if command finished
    kill "$timer_pid" 2>/dev/null
    wait "$timer_pid" 2>/dev/null
    
    return $exit_code
}
