#!/usr/bin/env bash
# whp-luks-unlock-helper.sh — privilege-separated /docker LUKS unlock helper.
#
# Invoked by:
#   - scripts/whp-unlock-data-drive (admin CLI; runs as root)
#   - web-files/api/luks-unlock.php (apache user, via sudo with NOPASSWD
#     scoped to this exact path — see scripts/updates/staging/setup-luks-sudoers.sh)
#
# Reads the LUKS passphrase from stdin. NEVER pass it on argv — it would be
# visible in /proc/<pid>/cmdline to any local user.
#
# Output: JSON on stdout (for the web API), human prose on stderr (for the CLI).
# Exit codes:
#   0  unlocked + mounted (downstream services either started or absent)
#   1  wrong passphrase or cryptsetup error
#   2  LUKS not configured on this server (no-op)
#   3  mount failed after unlock (mapper closed back down)
#   4  mounted, but failed to start docker or boot-orchestrator
#   5  internal error / preconditions failed

set -euo pipefail

# Source state helpers from the same directory as this script.
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
# shellcheck disable=SC1091
. "$SCRIPT_DIR/lib-luks-state.sh"

# Emit a single JSON object on stdout. Keeps the web caller's parsing trivial.
# Extra fields go in as KEY=VALUE positional args.
emit_json() {
    local status="$1" code="$2" msg="$3"
    shift 3
    printf '{"status":"%s","exit_code":%d,"message":"%s"' \
        "$status" "$code" "${msg//\"/\\\"}"
    while [ $# -gt 0 ]; do
        local kv="$1"
        local k="${kv%%=*}"
        local v="${kv#*=}"
        printf ',"%s":"%s"' "$k" "${v//\"/\\\"}"
        shift
    done
    printf '}\n'
}

# Already-unlocked is a benign no-op — return 0 so callers can be idempotent.
if ! luks_is_enabled; then
    echo "ERROR: LUKS is not configured on this server (/etc/whp/luks.conf missing)" >&2
    emit_json "not_configured" 2 "LUKS is not configured on this server"
    exit 2
fi
if luks_is_unlocked; then
    echo "INFO: /docker is already unlocked — nothing to do" >&2
    emit_json "already_unlocked" 0 "Already unlocked" "mount_point=$LUKS_MOUNT_POINT"
    exit 0
fi

# Passphrase from stdin only. Single line; reject empty.
IFS= read -r LUKS_PASSPHRASE || true
if [ -z "${LUKS_PASSPHRASE:-}" ]; then
    echo "ERROR: no passphrase on stdin" >&2
    emit_json "empty_passphrase" 5 "No passphrase provided on stdin"
    exit 5
fi

# Prefer LUKS_DEV_UUID — device-path-based lookup is brittle because
# Linux disk naming (sda/sdb) can shift between reboots with virtio-scsi
# under qemu (we hit this on VM 406). The LUKS UUID lives in the header
# itself, so it's stable across renames, hot-add/remove, controller
# reorders. Fall back to the device path on older luks.conf without UUID.
if [ -n "${LUKS_DEV_UUID:-}" ]; then
    _luks_open_target="UUID=$LUKS_DEV_UUID"
else
    _luks_open_target="$LUKS_BACKING_DEVICE"
fi

# Step 1: validate the passphrase explicitly. --test-passphrase doesn't
# actually open the mapper — it just verifies the keyslot can decrypt.
# Doing this BEFORE the open lets us:
#   (a) give an accurate "wrong passphrase" error,
#   (b) handle the partial-unlock recovery case (mapper open but /docker
#       unmounted — see below) without bypassing passphrase validation.
echo "Validating passphrase..." >&2
if ! printf '%s' "$LUKS_PASSPHRASE" | cryptsetup open \
        --test-passphrase --type luks2 --key-file=- \
        "$_luks_open_target" >&2 2>&1; then
    unset LUKS_PASSPHRASE
    echo "ERROR: passphrase rejected by LUKS keyslot validation" >&2
    emit_json "wrong_passphrase" 1 "Unlock failed — wrong passphrase"
    exit 1
fi

# Step 2: open the mapper (or skip if already open from a prior partial
# unlock — e.g. mapper opened + mounted + then /docker got unmounted by
# something but mapper wasn't closed). Passphrase was just validated, so
# reusing an existing mapper is safe.
if [ -e "/dev/mapper/$LUKS_MAPPER_NAME" ]; then
    echo "INFO: /dev/mapper/$LUKS_MAPPER_NAME already exists; reusing" >&2
    unset LUKS_PASSPHRASE
else
    echo "Opening $_luks_open_target as /dev/mapper/$LUKS_MAPPER_NAME..." >&2
    if ! printf '%s' "$LUKS_PASSPHRASE" | cryptsetup open \
            --type luks2 --key-file=- \
            "$_luks_open_target" "$LUKS_MAPPER_NAME" >&2 2>&1; then
        unset LUKS_PASSPHRASE
        echo "ERROR: cryptsetup open failed unexpectedly after passphrase validation passed" >&2
        emit_json "open_failed" 1 "Mapper open failed unexpectedly"
        exit 1
    fi
    unset LUKS_PASSPHRASE
fi

echo "Mounting on $LUKS_MOUNT_POINT..." >&2
if ! mount "/dev/mapper/$LUKS_MAPPER_NAME" "$LUKS_MOUNT_POINT" >&2 2>&1; then
    echo "ERROR: mount failed. Tearing down the mapper to leave state clean." >&2
    cryptsetup close "$LUKS_MAPPER_NAME" 2>/dev/null || true
    emit_json "mount_failed" 3 "Mount failed after unlock; mapper closed"
    exit 3
fi

# Best-effort downstream services. If docker is installed (normal WHP server)
# start it now; same for the boot orchestrator (if it exists yet — early in
# the LUKS rollout it may not). Don't unmount on service-start failure —
# admin can investigate with /docker still up.
STARTED=""
FAILED=""
for svc in docker httpd whp-boot-orchestrator; do
    if systemctl cat "$svc.service" >/dev/null 2>&1; then
        # Rescue from prior failed state — boot orchestrator's Tier 0 LUKS
        # check exits 2 on every locked reboot, so the unit ends up failed
        # after systemd's retries. Reset before start so the next attempt
        # actually runs. No-op if the unit isn't failed.
        systemctl reset-failed "$svc.service" 2>/dev/null || true
        if systemctl start "$svc.service" >&2 2>&1; then
            STARTED="$STARTED $svc"
            echo "Started $svc" >&2
        else
            FAILED="$FAILED $svc"
            echo "WARN: failed to start $svc" >&2
        fi
    fi
done

if [ -n "$FAILED" ]; then
    emit_json "service_start_failed" 4 "Mounted, but service start failed:${FAILED}" \
        "started=${STARTED# }" "failed=${FAILED# }"
    exit 4
fi

emit_json "unlocked" 0 "/docker unlocked and downstream services started" \
    "started=${STARTED# }" "mount_point=$LUKS_MOUNT_POINT"
exit 0
