#!/usr/bin/env bash
set -euo pipefail

# SocNetV performance micro-benchmark runner (macOS/Linux)
#
# Usage:
#   ./scripts/run_benchmarks.sh
#   ./scripts/run_benchmarks.sh --type distance
#   ./scripts/run_benchmarks.sh --type io
#   ./scripts/run_benchmarks.sh --type clustering
#   ./scripts/run_benchmarks.sh --strict
#   ./scripts/run_benchmarks.sh --record
#   SOCNETV_CLI=./build/socnetv-cli ./scripts/run_benchmarks.sh
#
# Baseline selection priority:
#   1) BENCH_BASELINE=/path/to/perf_expected.env
#   2) BENCH_BASELINE_SET=<name> (scripts/perf_baselines/<name>/perf_expected.env)
#   3) auto: <OS>-<ARCH> (scripts/perf_baselines/<auto>/perf_expected.env)

RECORD=0
STRICT=0
BENCH_TYPE="all"   # all|distance|io|clustering
LARGE_NETS_DIR="${HOME}/socnetv/library/nets/large"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --strict) STRICT=1; shift ;;
    --record) RECORD=1; shift ;;
    --type)
      [[ $# -ge 2 ]] || { echo "ERROR: --type requires an argument" >&2; exit 2; }
      BENCH_TYPE="$2"
      shift 2
      ;;
    *)
      echo "ERROR: unknown argument: $1" >&2
      exit 2
      ;;
  esac
done

case "$BENCH_TYPE" in
  all|distance|io|clustering) ;;
  *)
    echo "ERROR: invalid --type: $BENCH_TYPE (expected: all|distance|io|clustering)" >&2
    exit 2
    ;;
esac

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

BENCH_BUILD_TYPE="${BENCH_BUILD_TYPE:-Debug}"  # Debug|Release (hint only)

# shellcheck source=/dev/null
. "$ROOT_DIR/scripts/lib/find_socnetv_cli.sh"

if [[ -n "${SOCNETV_CLI:-}" ]]; then
  if [[ ! -x "$SOCNETV_CLI" ]]; then
    echo "ERROR: SOCNETV_CLI is set but not executable: $SOCNETV_CLI" >&2
    exit 1
  fi
else
  SOCNETV_CLI="$(find_socnetv_cli "$ROOT_DIR" "$BENCH_BUILD_TYPE" 2>/dev/null || true)"
fi

if [[ -z "${SOCNETV_CLI:-}" || ! -x "$SOCNETV_CLI" ]]; then
  echo "ERROR: Could not find socnetv-cli." >&2
  echo "Hint: SOCNETV_CLI=/full/path/to/socnetv-cli $0" >&2
  exit 1
fi

echo "[bench] Using SOCNETV_CLI=$SOCNETV_CLI"

BASELINE_ROOT="$ROOT_DIR/scripts/perf_baselines"

sanitize_id() {
  echo "$1" | tr -cd '[:alnum:]._-' | sed 's/^\.*//; s/\.*$//'
}

auto_baseline_set() {
  local os arch
  os="$(uname -s 2>/dev/null || echo unknownOS)"
  arch="$(uname -m 2>/dev/null || echo unknownARCH)"

  # Normalize OS naming a bit
  case "$os" in
    Darwin) os="macos" ;;
    Linux)  os="linux" ;;
    *)      os="$(echo "$os" | tr '[:upper:]' '[:lower:]')" ;;
  esac

  sanitize_id "${os}-${arch}"
}

resolve_expected_file() {
  if [[ -n "${BENCH_BASELINE:-}" ]]; then
    echo "${BENCH_BASELINE}"
    return 0
  fi

  local set_name="${BENCH_BASELINE_SET:-$(auto_baseline_set)}"
  set_name="$(sanitize_id "${set_name}")"

  local candidate="${BASELINE_ROOT}/${set_name}/perf_expected.env"
  if [[ -f "${candidate}" ]]; then
    echo "${candidate}"
    return 0
  fi

  echo "ERROR: no baseline found for set '${set_name}'" >&2
  echo "Expected: ${candidate}" >&2
  exit 2
}

EXPECTED_FILE="$(resolve_expected_file)"
RESOLVED_SET="$(sanitize_id "${BENCH_BASELINE_SET:-$(auto_baseline_set)}")"

if [[ -n "${BENCH_BASELINE:-}" ]]; then
  BASELINE_SET_USED="custom"
else
  BASELINE_SET_USED="${RESOLVED_SET}"
fi

if [[ "${RECORD}" == "1" && -n "${BENCH_BASELINE_SET:-}" && "${BENCH_RECORD_ALLOW_OVERRIDE:-0}" != "1" ]]; then
  AUTO_SET="$(auto_baseline_set)"
  if [[ "${RESOLVED_SET}" != "${AUTO_SET}" ]]; then
    echo "ERROR: refusing to --record into BENCH_BASELINE_SET=${BENCH_BASELINE_SET} (auto=${AUTO_SET})." >&2
    echo "Run on the target machine, or set BENCH_RECORD_ALLOW_OVERRIDE=1 to force." >&2
    exit 2
  fi
fi

echo "INFO: BENCH_BASELINE_SET=${BASELINE_SET_USED} EXPECTED_FILE=${EXPECTED_FILE} BUILD_TYPE=${BENCH_BUILD_TYPE} RECORD=${RECORD}" >&2

if [[ ! -x "$SOCNETV_CLI" ]]; then
  echo "ERROR: socnetv-cli not found or not executable: $SOCNETV_CLI" >&2
  echo "Set SOCNETV_CLI=/path/to/socnetv-cli" >&2
  exit 2
fi

if [[ "${RECORD}" != "1" ]]; then
  if [[ ! -f "$EXPECTED_FILE" ]]; then
    echo "ERROR: expected perf file missing: $EXPECTED_FILE" >&2
    exit 2
  fi
  # shellcheck disable=SC1090
  source "$EXPECTED_FILE"
fi

REC_LINES=()

extract_kv_int() {
  local key="$1"
  awk -F= -v k="$key" '$1==k { print $2 }'
}

within_threshold() {
  local actual="$1"
  local expected="$2"
  [[ -z "$actual" || -z "$expected" ]] && return 1
  if (( 100 * actual > 110 * expected )); then
    return 1
  fi
  return 0
}

record_expected_var() {
  local tag="$1"
  local median="$2"
  local expected_var="EXP_${tag}_MEDIAN_MS"
  REC_LINES+=("${expected_var}=${median}")
}

write_record_file() {
  local out_file="$1"
  local rel="$out_file"
  rel="${rel#${ROOT_DIR}/}"

  {
    echo "# Auto-generated by scripts/run_benchmarks.sh --record"
    echo "# BASELINE_SET=$(sanitize_id "${BASELINE_SET_USED}")"
    echo "# BASELINE_FILE=${rel}"
    echo "# BUILD_TYPE=${BENCH_BUILD_TYPE}"
    echo
    printf '%s\n' "${REC_LINES[@]}" | LC_ALL=C sort
  } > "$out_file"
}

run_case_io_bench() {
  local tag="$1"
  local input="$2"
  local ftype="$3"
  shift 3

  echo "=== $tag ==="

  local out
  if ! out="$("$SOCNETV_CLI" --kernel io_roundtrip -i "$input" -f "$ftype" "$@" 2>/dev/null)"; then
    echo "ERROR: socnetv-cli failed for $tag" >&2
    return 2
  fi

  local ok load_ms
  ok="$(printf '%s\n' "$out" | extract_kv_int LOAD_OK | tail -n1)"
  load_ms="$(printf '%s\n' "$out" | extract_kv_int LOAD_MS | tail -n1)"

  echo "OK=$ok LOAD_MS=$load_ms"

  if [[ "$ok" != "1" || -z "$load_ms" ]]; then
    echo "ERROR: io_roundtrip benchmark did not produce valid output for $tag" >&2
    return 2
  fi

  if [[ "${RECORD}" == "1" ]]; then
    record_expected_var "$tag" "$load_ms"
    echo "OK: recorded ${load_ms}ms"
    return 0
  fi

  local expected_var="EXP_${tag}_MEDIAN_MS"
  local expected="${!expected_var:-}"

  if [[ -z "$expected" ]]; then
    echo "WARN: no expected load_ms configured for $tag"
    return 0
  fi

  # Skip enforcement if baseline is 0 (too fast to measure reliably)
  if (( expected == 0 )); then
    echo "TIMING_SKIP: baseline=0ms, not enforced for $tag"
    return 0
  fi

  local allowed pct_delta pct_str
  allowed=$(( expected + (expected / 10) ))

  # Percent delta vs baseline: negative = faster, positive = slower
  if (( expected > 0 )); then
    pct_delta=$(( (load_ms - expected) * 100 / expected ))
  else
    pct_delta=0
  fi
  if (( pct_delta < 0 )); then
    pct_str="${pct_delta#-}% faster than"
  elif (( pct_delta > 0 )); then
    pct_str="+${pct_delta}% slower than"
  else
    pct_str="at"
  fi

  if within_threshold "$load_ms" "$expected"; then
    echo "OK: ${load_ms}ms <= ${allowed}ms, ${pct_str} baseline ${expected}ms (+10% threshold)"
    return 0
  else
    echo "FAIL: ${load_ms}ms > ${allowed}ms, ${pct_str} baseline ${expected}ms (+10% threshold)"
    [[ "$STRICT" == "1" ]] && return 1
    return 0
  fi
}


run_case() {
  local tag="$1"
  shift

  echo "=== $tag ==="

  # We want stable output (suppress stderr), but still detect failures.
  local out
  if ! out="$("$SOCNETV_CLI" "$@" 2>/dev/null)"; then
    echo "ERROR: socnetv-cli failed for $tag" >&2
    return 2
  fi

  local ok median
  ok="$(printf '%s\n' "$out" | extract_kv_int LOAD_OK | tail -n1)"
  median="$(printf '%s\n' "$out" | extract_kv_int COMPUTE_MS_MEDIAN | tail -n1)"

  echo "OK=$ok MEDIAN_MS=$median"

  if [[ "$ok" != "1" || -z "$median" ]]; then
    echo "ERROR: benchmark did not produce valid output for $tag" >&2
    return 2
  fi

  if [[ "${RECORD}" == "1" ]]; then
    record_expected_var "$tag" "$median"
    echo "OK: recorded ${median}ms"
    return 0
  fi

  local expected_var="EXP_${tag}_MEDIAN_MS"
  local expected="${!expected_var:-}"

  if [[ -z "$expected" ]]; then
    echo "WARN: no expected median configured for $tag"
    return 0
  fi

  local allowed pct_delta pct_str
  # allowed threshold = expected + 10%
  allowed=$(( expected + (expected / 10) ))

  # Percent delta vs baseline: negative = faster, positive = slower
  if (( expected > 0 )); then
    pct_delta=$(( (median - expected) * 100 / expected ))
  else
    pct_delta=0
  fi
  if (( pct_delta < 0 )); then
    pct_str="${pct_delta#-}% faster than"
  elif (( pct_delta > 0 )); then
    pct_str="+${pct_delta}% slower than"
  else
    pct_str="at"
  fi

  if within_threshold "$median" "$expected"; then
    echo "OK: ${median}ms <= ${allowed}ms, ${pct_str} baseline ${expected}ms (+10% threshold)"
    return 0
  else
    echo "FAIL: ${median}ms > ${allowed}ms, ${pct_str} baseline ${expected}ms (+10% threshold)"
    [[ "$STRICT" == "1" ]] && return 1
    return 0
  fi
}

fail=0

should_run_type() {
  local wanted="$1"
  if [[ "$BENCH_TYPE" == "$wanted" ]]; then
    return 0
  fi

  if [[ "$BENCH_TYPE" == "all" ]]; then
    [[ "$wanted" == "distance" || "$wanted" == "io" ]]
    return
  fi

  return 1
}

# ---------------- benchmark cases ----------------
# NOTE: CLI enforces --bench only for --kernel distance (default kernel is distance).

if should_run_type distance; then

  # Medium (UCINET time_1)
  run_case "EIES48_T1_C1_W1" \
    -i "$ROOT_DIR/src/data/Freeman_EIES_network_48actors_Acquaintanceship_at_time_1.dl" \
    -f 5 -c 1 -w 1 -x 1 -k 0 --bench 20 || fail=1

  # Medium (UCINET time_2)
  run_case "EIES48_T2_C1_W1" \
    -i "$ROOT_DIR/src/data/Freeman_EIES_network_48actors_Acquaintanceship_at_time_2.dl" \
    -f 5 -c 1 -w 1 -x 1 -k 0 --bench 20 || fail=1

  # Large BA (500, m=3), centralities ON
  run_case "BA500_M3_C1_W0" \
    -i "$ROOT_DIR/src/data/Benchmark_BA_Directed_N500_m3.paj" \
    -f 2 -c 1 -w 0 -x 1 -k 0 --bench 20 || fail=1

  # Large BA (500, m=3), distances only
  run_case "BA500_M3_C0_W0" \
    -i "$ROOT_DIR/src/data/Benchmark_BA_Directed_N500_m3.paj" \
    -f 2 -c 0 -w 0 -x 1 -k 0 --bench 20 || fail=1

  # ---------------- large-net distance benchmark cases ----------------
  # Only run if ~/socnetv/library/nets/large/ exists (not shipped with repo).
  # BA500 cases above are always the CI-reproducible fallback.

  if [[ -d "$LARGE_NETS_DIR" ]]; then
    run_case "DIST_GRAPHML_1000N_10000A_C0_W0" \
      -i "$LARGE_NETS_DIR/1000actors-10000arcs.graphml" \
      -f 1 -c 0 -w 0 -x 1 -k 0 --bench 2 || fail=1

    run_case "DIST_GRAPHML_1000N_10000A_C1_W0" \
      -i "$LARGE_NETS_DIR/1000actors-10000arcs.graphml" \
      -f 1 -c 1 -w 0 -x 1 -k 0 --bench 2 || fail=1

  else
    echo "[bench] LARGE_NETS_DIR not found ($LARGE_NETS_DIR), skipping large-net distance benchmarks" >&2
  fi

fi

# ---------------- IO roundtrip benchmark cases ----------------
# Only large nets (N>=50) are meaningful for IO timing.
# Uses ~/socnetv/library/nets/large/ if available, falls back to src/data/.
if should_run_type io; then
  # Always run the shipped dataset (reproducible in CI)
  run_case_io_bench "IO_PAJ_BA500" \
    "$ROOT_DIR/src/data/Benchmark_BA_Directed_N500_m3.paj" 2 || fail=1

  if [[ -d "$LARGE_NETS_DIR" ]]; then
    run_case_io_bench "IO_GRAPHML_2000N_40000E" \
      "$LARGE_NETS_DIR/2000actors-40000edges.graphml" 1 || fail=1
    run_case_io_bench "IO_GRAPHML_1000N_10000A" \
      "$LARGE_NETS_DIR/1000actors-10000arcs.graphml" 1 || fail=1
  else
    echo "[bench] LARGE_NETS_DIR not found ($LARGE_NETS_DIR), skipping large-net IO benchmarks" >&2
  fi
fi

# ---------------- optional clustering timing (non-bench) ----------------
# NOTE:
# - clustering kernel does NOT support --bench
# - this is informational timing only (single run)
# - not enforced in STRICT mode
# - runs only with --type clustering

run_case_clustering() {
  local tag="$1"
  shift

  echo "=== $tag ==="

  local start end ms out ok
  start=$(date +%s%3N 2>/dev/null || date +%s)

  if ! out="$("$SOCNETV_CLI" --kernel clustering "$@" 2>/dev/null)"; then
    echo "ERROR: socnetv-cli failed for $tag" >&2
    return 2
  fi

  end=$(date +%s%3N 2>/dev/null || date +%s)

  if [[ ${#start} -le 10 ]]; then
    ms=$(( (end - start) * 1000 ))
  else
    ms=$(( end - start ))
  fi

  ok="$(printf '%s\n' "$out" | extract_kv_int LOAD_OK | tail -n1)"

  echo "OK=$ok CLUSTERING_MS=$ms"

  if [[ "$ok" != "1" ]]; then
    echo "ERROR: clustering run failed for $tag" >&2
    return 2
  fi

  return 0
}

if should_run_type clustering; then
  echo "[bench] Running clustering timing probes (informational only)" >&2

  run_case_clustering "CLUST_KITE_N10" \
    -i "$ROOT_DIR/src/data/Krackhardt_Kite_N10.paj" \
    -f 2 -w 0 -x 1 -k 0 || fail=1

  run_case_clustering "CLUST_SAMPSON_N18" \
    -i "$ROOT_DIR/src/data/Sampson_Monks_N18.net" \
    -f 2 -w 0 -x 1 -k 0 || fail=1
fi

echo "=== DONE ==="

if [[ "${RECORD}" == "1" ]]; then
  if [[ -n "${BENCH_BASELINE:-}" ]]; then
    OUT_FILE="${BENCH_BASELINE}"
  else
    SET_DIR="${BASELINE_ROOT}/$(sanitize_id "${BASELINE_SET_USED}")"
    mkdir -p "${SET_DIR}"
    OUT_FILE="${SET_DIR}/perf_expected.env"
  fi
  write_record_file "${OUT_FILE}"
  echo "INFO: wrote baseline: ${OUT_FILE}" >&2
  echo "RESULT=OK"
  exit 0
fi

if [[ "$fail" == "1" ]]; then
  echo "RESULT=FAIL"
  exit 1
fi

echo "RESULT=OK"
exit 0