#!/usr/bin/env bash
#
# One-touch, fully offline build of prometheus-client-c (libprom + libpromhttp)
# together with its runtime dependency libmicrohttpd.
#
# Everything needed is taken from ./deps and ./patches; no network access is used.
# Run  ./build.sh --help  for options.
#
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPS="$ROOT/deps"
PATCHES="$ROOT/patches"

MHD_VERSION="1.0.10"
PROM_VERSION="0.1.3"
BUNDLED_CMAKE_VERSION="3.31.8"
MIN_CMAKE="3.14.5"

PREFIX="$ROOT/install"
WORK="$ROOT/build"
JOBS="$(nproc 2>/dev/null || echo 2)"
RUN_TESTS=0
RUN_SMOKE=1
FORCE_BUNDLED_CMAKE=0
NO_WERROR=0
CLEAN=0

usage() {
  cat <<EOF
Usage: $0 [options]

  --prefix DIR        install location (default: $ROOT/install)
  --jobs N            parallel make jobs (default: $JOBS)
  --tests             also build and run the upstream libprom unit tests
  --no-smoke          skip the post-install smoke test
  --bundled-cmake     always use the bundled CMake instead of the system one
  --no-werror         strip -Werror from upstream flags (for newer compilers)
  --clean             remove the build directory and install prefix first
  -h, --help          show this help
EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --prefix)        PREFIX="$2"; shift 2 ;;
    --prefix=*)      PREFIX="${1#*=}"; shift ;;
    --jobs|-j)       JOBS="$2"; shift 2 ;;
    --jobs=*)        JOBS="${1#*=}"; shift ;;
    --tests)         RUN_TESTS=1; shift ;;
    --no-smoke)      RUN_SMOKE=0; shift ;;
    --bundled-cmake) FORCE_BUNDLED_CMAKE=1; shift ;;
    --no-werror)     NO_WERROR=1; shift ;;
    --clean)         CLEAN=1; shift ;;
    -h|--help)       usage; exit 0 ;;
    *) echo "Unknown option: $1" >&2; usage; exit 2 ;;
  esac
done

mkdir -p "$PREFIX"
PREFIX="$(cd "$PREFIX" && pwd)"
LOG_DIR="$WORK/logs"

step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
die()  { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }

# Run a command with output going to a log file; show the tail on failure.
run_logged() {
  local log="$LOG_DIR/$1"; shift
  if ! "$@" >"$log" 2>&1; then
    tail -n 40 "$log" >&2
    die "command failed: $* (full log: $log)"
  fi
}

version_ge() { [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]]; }

# ---------------------------------------------------------------- preflight
step "Checking prerequisites"
for tool in gcc make tar gzip sed sort; do
  command -v "$tool" >/dev/null 2>&1 || die "'$tool' is required but not installed"
done
command -v patch >/dev/null 2>&1 || command -v git >/dev/null 2>&1 || die "'patch' (or git) is required but not installed"

MHD_TGZ="$DEPS/sources/libmicrohttpd-$MHD_VERSION.tar.gz"
PROM_TGZ="$(ls "$DEPS"/sources/prometheus-client-c-"$PROM_VERSION"-*.tar.gz 2>/dev/null | head -n1 || true)"
CMAKE_TGZ="$DEPS/tools/cmake-$BUNDLED_CMAKE_VERSION-linux-x86_64.tar.gz"
[[ -f "$MHD_TGZ" ]]  || die "missing $MHD_TGZ"
[[ -n "$PROM_TGZ" ]] || die "missing prometheus-client-c source tarball in $DEPS/sources"

if [[ -f "$DEPS/SHA256SUMS" ]]; then
  (cd "$DEPS" && sha256sum --quiet -c SHA256SUMS) || die "checksum verification of deps/ failed"
  echo "deps/ checksums OK"
fi

if [[ $CLEAN -eq 1 ]]; then
  rm -rf "$WORK" "$PREFIX"
  mkdir -p "$PREFIX"
fi
rm -rf "$WORK/src"
mkdir -p "$WORK/src" "$LOG_DIR"

# Pick CMake: system one if new enough, otherwise the bundled binary.
CMAKE=""
if [[ $FORCE_BUNDLED_CMAKE -eq 0 ]] && command -v cmake >/dev/null 2>&1; then
  sys_ver="$(cmake --version | head -n1 | awk '{print $3}')"
  if version_ge "$sys_ver" "$MIN_CMAKE"; then
    CMAKE="$(command -v cmake)"
    echo "Using system CMake $sys_ver"
  fi
fi
if [[ -z "$CMAKE" ]]; then
  [[ "$(uname -m)" == "x86_64" ]] || die "no suitable system CMake (>= $MIN_CMAKE) and bundled CMake is x86_64 only"
  [[ -f "$CMAKE_TGZ" ]] || die "no suitable system CMake and bundled $CMAKE_TGZ is missing"
  if [[ ! -x "$WORK/cmake/bin/cmake" ]]; then
    rm -rf "$WORK/cmake" && mkdir -p "$WORK/cmake"
    tar -xzf "$CMAKE_TGZ" -C "$WORK/cmake" --strip-components=1
  fi
  CMAKE="$WORK/cmake/bin/cmake"
  echo "Using bundled CMake $BUNDLED_CMAKE_VERSION"
fi
CTEST="$(dirname "$CMAKE")/ctest"

# ---------------------------------------------------------------- libmicrohttpd
step "Building libmicrohttpd $MHD_VERSION"
tar -xzf "$MHD_TGZ" -C "$WORK/src"
MHD_SRC="$WORK/src/libmicrohttpd-$MHD_VERSION"
(
  cd "$MHD_SRC"
  run_logged mhd-configure.log ./configure \
    --prefix="$PREFIX" --libdir="$PREFIX/lib" \
    --disable-https --without-gnutls \
    --disable-doc --disable-examples --disable-curl \
    --enable-shared --enable-static
  run_logged mhd-make.log make -j"$JOBS"
  run_logged mhd-install.log make install
)
# Make the .pc file relocatable like the ones generated below
sed -i -e 's#^prefix=.*#prefix=${pcfiledir}/../..#' -e "s#$PREFIX#\${prefix}#g" "$PREFIX/lib/pkgconfig/libmicrohttpd.pc"

# ---------------------------------------------------------------- prometheus-client-c
step "Preparing prometheus-client-c $PROM_VERSION ($(basename "$PROM_TGZ"))"
tar -xzf "$PROM_TGZ" -C "$WORK/src"
PROM_SRC="$WORK/src/prometheus-client-c"
for p in "$PATCHES"/*.patch; do
  [[ -e "$p" ]] || continue
  echo "Applying $(basename "$p")"
  if command -v patch >/dev/null 2>&1; then
    patch -d "$PROM_SRC" -p1 --silent < "$p"
  else
    git -C "$PROM_SRC" apply "$p"
  fi
done
if [[ $NO_WERROR -eq 1 ]]; then
  sed -i 's/"-Werror" //g' "$PROM_SRC"/prom/CMakeLists.txt "$PROM_SRC"/promhttp/CMakeLists.txt
fi

CMAKE_COMMON=(
  -DCMAKE_BUILD_TYPE=Release
  -DCMAKE_INSTALL_PREFIX="$PREFIX"
  -DCMAKE_INSTALL_LIBDIR=lib
  -DCMAKE_PREFIX_PATH="$PREFIX"
  -DCMAKE_INSTALL_RPATH='$ORIGIN'
  -DCMAKE_BUILD_WITH_INSTALL_RPATH=OFF
)

step "Building libprom"
# Upstream promhttp expects libprom in prom/build, so keep that layout.
run_logged prom-configure.log "$CMAKE" -S "$PROM_SRC/prom" -B "$PROM_SRC/prom/build" "${CMAKE_COMMON[@]}"
run_logged prom-build.log "$CMAKE" --build "$PROM_SRC/prom/build" -j "$JOBS"
run_logged prom-install.log "$CMAKE" --install "$PROM_SRC/prom/build"

step "Building libpromhttp"
CFLAGS="-I$PREFIX/include ${CFLAGS:-}" LDFLAGS="-L$PREFIX/lib ${LDFLAGS:-}" \
  run_logged promhttp-configure.log "$CMAKE" -S "$PROM_SRC/promhttp" -B "$PROM_SRC/promhttp/build" "${CMAKE_COMMON[@]}"
run_logged promhttp-build.log "$CMAKE" --build "$PROM_SRC/promhttp/build" -j "$JOBS"
run_logged promhttp-install.log "$CMAKE" --install "$PROM_SRC/promhttp/build"

# pkg-config files so consumers can use `pkg-config --cflags --libs libpromhttp`
mkdir -p "$PREFIX/lib/pkgconfig"
cat > "$PREFIX/lib/pkgconfig/libprom.pc" <<EOF
prefix=\${pcfiledir}/../..
libdir=\${prefix}/lib
includedir=\${prefix}/include

Name: libprom
Description: Prometheus client library for C
Version: $PROM_VERSION
Libs: -L\${libdir} -Wl,-rpath,\${libdir} -lprom
Libs.private: -pthread
Cflags: -I\${includedir}
EOF
cat > "$PREFIX/lib/pkgconfig/libpromhttp.pc" <<EOF
prefix=\${pcfiledir}/../..
libdir=\${prefix}/lib
includedir=\${prefix}/include

Name: libpromhttp
Description: HTTP exposition of Prometheus metrics for libprom (libmicrohttpd)
Version: $PROM_VERSION
Requires: libprom libmicrohttpd
Libs: -L\${libdir} -Wl,-rpath,\${libdir} -lpromhttp
Cflags: -I\${includedir}
EOF

# ---------------------------------------------------------------- tests
if [[ $RUN_TESTS -eq 1 ]]; then
  step "Running upstream libprom unit tests"
  # Upstream tests hardcode fixture paths of their Docker image (/code/...)
  sed -i "s#/code/prom/#$PROM_SRC/prom/#g" "$PROM_SRC"/prom/test/*.c
  TEST=1 run_logged test-configure.log "$CMAKE" -S "$PROM_SRC/prom" -B "$WORK/prom-test" -DCMAKE_BUILD_TYPE=Debug
  run_logged test-build.log "$CMAKE" --build "$WORK/prom-test" -j "$JOBS"
  # Tests read fixtures via relative paths from the prom/ directory
  (cd "$PROM_SRC/prom" && run_logged test-run.log "$CTEST" --test-dir "$WORK/prom-test" --output-on-failure)
  tail -n 3 "$LOG_DIR/test-run.log"
fi

if [[ $RUN_SMOKE -eq 1 ]]; then
  step "Smoke test"
  SMOKE_BIN="$WORK/smoke_test"
  gcc -std=gnu11 -Wall -Werror -o "$SMOKE_BIN" "$ROOT/smoke/smoke_test.c" \
    -I"$PREFIX/include" -L"$PREFIX/lib" -Wl,-rpath,"$PREFIX/lib" -lpromhttp -lprom -lmicrohttpd -pthread
  out="$("$SMOKE_BIN")" || die "smoke test failed"
  grep -q 'smoke_requests_total{path="/api"} 3' <<<"$out" || die "smoke test produced unexpected output"
  echo "libprom OK (metrics rendered)"
  if command -v curl >/dev/null 2>&1; then
    port=$(( 20000 + RANDOM % 20000 ))
    "$SMOKE_BIN" "$port" 3 >/dev/null & pid=$!
    ok=0
    for _ in 1 2 3 4 5; do
      sleep 0.5
      if curl -fsS "http://127.0.0.1:$port/metrics" 2>/dev/null | grep -q '^smoke_temperature 21.5'; then ok=1; break; fi
    done
    if [[ $ok -eq 1 ]]; then
      echo "libpromhttp OK (served /metrics on port $port)"
    else
      echo "WARNING: HTTP check on port $port failed (port in use?); libraries are installed anyway" >&2
    fi
    wait "$pid" || true
  fi
fi

step "Done"
cat <<EOF
Installed into: $PREFIX
  headers : $PREFIX/include   (prom.h, promhttp.h, microhttpd.h, ...)
  libs    : $PREFIX/lib       (libprom.so, libpromhttp.so, libmicrohttpd.so/.a)
  pkgconf : $PREFIX/lib/pkgconfig

Compile your program with:
  gcc app.c -I$PREFIX/include -L$PREFIX/lib -Wl,-rpath,$PREFIX/lib -lpromhttp -lprom -lmicrohttpd -pthread
or:
  PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig gcc app.c \$(pkg-config --cflags --libs libpromhttp) -pthread
EOF
