#!/bin/sh
set -eu

DURATION_SEC=300
STAGGER_SECONDS=0
DEVICES=""
EXCLUDE=""
FORMAT=""
TEST_SLICE_SECONDS=300
JOHN_DIR="/usr/local/lib/bee/john/run"
JOHN_BIN="${JOHN_DIR}/john"
export OCL_ICD_VENDORS="/etc/OpenCL/vendors"
export LD_LIBRARY_PATH="/usr/lib:/usr/local/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"

usage() {
    echo "usage: $0 [--seconds N] [--stagger-seconds N] [--devices 0,1] [--exclude 2,3] [--format name]" >&2
    exit 2
}

normalize_list() {
    echo "${1:-}" | tr ',' '\n' | sed 's/[[:space:]]//g' | awk 'NF' | sort -n | uniq | paste -sd, -
}

contains_csv() {
    needle="$1"
    haystack="${2:-}"
    echo ",${haystack}," | grep -q ",${needle},"
}

show_opencl_diagnostics() {
    echo "-- OpenCL ICD vendors --" >&2
    if [ -d /etc/OpenCL/vendors ]; then
        ls -l /etc/OpenCL/vendors >&2 || true
        for icd in /etc/OpenCL/vendors/*.icd; do
            [ -f "${icd}" ] || continue
            echo "  file: ${icd}" >&2
            sed 's/^/    /' "${icd}" >&2 || true
        done
    else
        echo "  /etc/OpenCL/vendors is missing" >&2
    fi
    echo "-- NVIDIA device nodes --" >&2
    ls -l /dev/nvidia* >&2 || true
    echo "-- ldconfig OpenCL/NVIDIA --" >&2
    ldconfig -p 2>/dev/null | grep 'libOpenCL\|libcuda\|libnvidia-opencl' >&2 || true
    if command -v clinfo >/dev/null 2>&1; then
        echo "-- clinfo -l --" >&2
        clinfo -l >&2 || true
    fi
    echo "-- john --list=opencl-devices --" >&2
    ./john --list=opencl-devices >&2 || true
}

refresh_nvidia_runtime() {
    if [ "$(id -u)" != "0" ]; then
        return 1
    fi
    if command -v bee-nvidia-load >/dev/null 2>&1; then
        bee-nvidia-load >/dev/null 2>&1 || true
    fi
    ldconfig >/dev/null 2>&1 || true
    return 0
}

ensure_nvidia_uvm() {
    if lsmod 2>/dev/null | grep -q '^nvidia_uvm '; then
        return 0
    fi
    if [ "$(id -u)" != "0" ]; then
        return 1
    fi

    ko="/usr/local/lib/nvidia/nvidia-uvm.ko"
    [ -f "${ko}" ] || return 1

    if ! insmod "${ko}" >/dev/null 2>&1; then
        return 1
    fi

    uvm_major=$(grep -m1 ' nvidia-uvm$' /proc/devices | awk '{print $1}')
    if [ -n "${uvm_major}" ]; then
        mknod -m 666 /dev/nvidia-uvm c "${uvm_major}" 0 2>/dev/null || true
        mknod -m 666 /dev/nvidia-uvm-tools c "${uvm_major}" 1 2>/dev/null || true
    fi
    return 0
}

ensure_opencl_ready() {
    out=$(./john --list=opencl-devices 2>&1 || true)
    if echo "${out}" | grep -q "Device #"; then
        return 0
    fi

    if refresh_nvidia_runtime; then
        out=$(./john --list=opencl-devices 2>&1 || true)
        if echo "${out}" | grep -q "Device #"; then
            return 0
        fi
    fi

    if ensure_nvidia_uvm; then
        out=$(./john --list=opencl-devices 2>&1 || true)
        if echo "${out}" | grep -q "Device #"; then
            return 0
        fi
    fi

    echo "OpenCL devices are not available for John." >&2
    if ! lsmod 2>/dev/null | grep -q '^nvidia_uvm '; then
        echo "nvidia_uvm is not loaded." >&2
    fi
    if [ ! -e /dev/nvidia-uvm ]; then
        echo "/dev/nvidia-uvm is missing." >&2
    fi
    show_opencl_diagnostics
    return 1
}

while [ "$#" -gt 0 ]; do
    case "$1" in
        --seconds|-t) [ "$#" -ge 2 ] || usage; DURATION_SEC="$2"; shift 2 ;;
        --stagger-seconds) [ "$#" -ge 2 ] || usage; STAGGER_SECONDS="$2"; shift 2 ;;
        --devices) [ "$#" -ge 2 ] || usage; DEVICES="$2"; shift 2 ;;
        --exclude) [ "$#" -ge 2 ] || usage; EXCLUDE="$2"; shift 2 ;;
        --format) [ "$#" -ge 2 ] || usage; FORMAT="$2"; shift 2 ;;
        *) usage ;;
    esac
done

[ -x "${JOHN_BIN}" ] || { echo "john binary not found: ${JOHN_BIN}" >&2; exit 1; }

ALL_DEVICES=$(nvidia-smi --query-gpu=index --format=csv,noheader,nounits 2>/dev/null | sed 's/[[:space:]]//g' | awk 'NF' | paste -sd, -)
[ -n "${ALL_DEVICES}" ] || { echo "nvidia-smi found no NVIDIA GPUs" >&2; exit 1; }

DEVICES=$(normalize_list "${DEVICES}")
EXCLUDE=$(normalize_list "${EXCLUDE}")
SELECTED="${DEVICES}"
if [ -z "${SELECTED}" ]; then
    SELECTED="${ALL_DEVICES}"
fi

FINAL=""
for id in $(echo "${SELECTED}" | tr ',' ' '); do
    [ -n "${id}" ] || continue
    if contains_csv "${id}" "${EXCLUDE}"; then
        continue
    fi
    if [ -z "${FINAL}" ]; then
        FINAL="${id}"
    else
        FINAL="${FINAL},${id}"
    fi
done

[ -n "${FINAL}" ] || { echo "no NVIDIA GPUs selected after filters" >&2; exit 1; }

export CUDA_DEVICE_ORDER="PCI_BUS_ID"
export CUDA_VISIBLE_DEVICES="${FINAL}"

JOHN_DEVICES=""
local_id=1
for id in $(echo "${FINAL}" | tr ',' ' '); do
    opencl_id="${local_id}"
    if [ -z "${JOHN_DEVICES}" ]; then
        JOHN_DEVICES="${opencl_id}"
    else
        JOHN_DEVICES="${JOHN_DEVICES},${opencl_id}"
    fi
    local_id=$((local_id + 1))
done

echo "loader=john"
echo "selected_gpus=${FINAL}"
echo "john_devices=${JOHN_DEVICES}"
echo "stagger_seconds=${STAGGER_SECONDS}"

cd "${JOHN_DIR}"

ensure_opencl_ready || exit 1

choose_format() {
    if [ -n "${FORMAT}" ]; then
        echo "${FORMAT}"
        return 0
    fi
    for candidate in sha512crypt-opencl pbkdf2-hmac-sha512-opencl 7z-opencl sha256crypt-opencl md5crypt-opencl; do
        if ./john --test=1 --format="${candidate}" --devices="${JOHN_DEVICES}" >/dev/null 2>&1; then
            echo "${candidate}"
            return 0
        fi
    done
    return 1
}

CHOSEN_FORMAT=$(choose_format) || {
    echo "no suitable john OpenCL format found" >&2
    ./john --list=opencl-devices >&2 || true
    exit 1
}

run_john_loop() {
    opencl_id="$1"
    deadline="$2"
    round=0
    while :; do
        now=$(date +%s)
        remaining=$((deadline - now))
        if [ "${remaining}" -le 0 ]; then
            break
        fi
        round=$((round + 1))
        slice="${remaining}"
        if [ "${slice}" -gt "${TEST_SLICE_SECONDS}" ]; then
            slice="${TEST_SLICE_SECONDS}"
        fi
        echo "device=${opencl_id} round=${round} remaining_sec=${remaining} slice_sec=${slice}"
        ./john --test="${slice}" --format="${CHOSEN_FORMAT}" --devices="${opencl_id}" || return 1
    done
}

PIDS=""
cleanup() {
    rc=$?
    trap - EXIT INT TERM
    for pid in ${PIDS}; do
        kill "${pid}" 2>/dev/null || true
    done
    for pid in ${PIDS}; do
        wait "${pid}" 2>/dev/null || true
    done
    exit "${rc}"
}
trap cleanup EXIT INT TERM

echo "format=${CHOSEN_FORMAT}"
echo "target_seconds=${DURATION_SEC}"
echo "slice_seconds=${TEST_SLICE_SECONDS}"
TOTAL_DEVICES=$(echo "${JOHN_DEVICES}" | tr ',' '\n' | awk 'NF' | wc -l | tr -d '[:space:]')
_first=1
pos=0
for opencl_id in $(echo "${JOHN_DEVICES}" | tr ',' ' '); do
    pos=$((pos + 1))
    [ "${_first}" = "1" ] || sleep 3
    _first=0
    extra_sec=$(( STAGGER_SECONDS * (TOTAL_DEVICES - pos) ))
    deadline=$(( $(date +%s) + DURATION_SEC + extra_sec ))
    run_john_loop "${opencl_id}" "${deadline}" &
    pid=$!
    PIDS="${PIDS} ${pid}"
    if [ "${STAGGER_SECONDS}" -gt 0 ] && [ "${pos}" -lt "${TOTAL_DEVICES}" ]; then
        sleep "${STAGGER_SECONDS}"
    fi
done
FAIL=0
for pid in ${PIDS}; do
    wait "${pid}" || FAIL=$((FAIL+1))
done
[ "${FAIL}" -eq 0 ] || { echo "john: ${FAIL} device(s) failed" >&2; exit 1; }
