#!/bin/bash
# Velometrik Remote Client Installer
# Zero-touch reverse SSH tunnel for Velobox / SmartCube
# Version 1.6.0-universal
#
# Rollout: run as root without arguments.
# First enrollment is fully automatic. A bootstrap enrollment credential is
# embedded in this rollout installer. It is used only for HTTPS enrollment and
# is never used by SSH. Existing station registrations cannot be overwritten
# through the enrollment API.

set -Eeuo pipefail
IFS=$'\n\t'

VERSION="1.6.0-universal"

CONFIG_FILE="${VMK_REMOTE_CONFIG_FILE:-/usr/local/etc/vmkstationd.conf}"
STATE_DIR="${VMK_REMOTE_STATE_DIR:-/var/lib/velometrik-remote}"
ETC_DIR="${VMK_REMOTE_ETC_DIR:-/etc/velometrik/remote}"
KEY_FILE="${STATE_DIR}/id_ed25519"
KNOWN_HOSTS_FILE="${ETC_DIR}/known_hosts"
CLIENT_CONF="${ETC_DIR}/client.conf"
ENROLLED_MARKER="${STATE_DIR}/enrolled"
RUNNER="/usr/local/sbin/vmk-remote-tunnel"
UNIT_FILE="/etc/systemd/system/vmk-remote.service"
SERVICE="vmk-remote.service"

SSH_PORT="${VMK_REMOTE_SSH_PORT:-443}"
LOCAL_SSH_HOST="${VMK_REMOTE_LOCAL_SSH_HOST:-127.0.0.1}"
LOCAL_SSH_PORT="${VMK_REMOTE_LOCAL_SSH_PORT:-22}"
REMOTE_USER="${VMK_REMOTE_USER:-root}"
REMOTE_BIND_ADDRESS="${VMK_REMOTE_BIND_ADDRESS:-127.0.0.1}"

# Enrollment API. The target host is derived from WS_URL.
ENROLL_PATH="${VMK_REMOTE_ENROLL_PATH:-/remote-api/enroll}"

# One installer for all supported Velometrik/ACID Fit stations.
# Server-specific host keys and enrollment credentials are selected only after
# REMOTE_HOST has been derived from /usr/local/etc/vmkstationd.conf.
SSH_HOSTKEY_TYPE="ssh-ed25519"
SSH_HOSTKEY_B64=""
SSH_HOSTKEY_SHA256=""
DEFAULT_ENROLL_TOKEN=""
REMOTE_PROFILE=""

select_remote_profile() {
    case "$REMOTE_HOST" in
        ws-1-1.velometrik.de)
            REMOTE_PROFILE="Velometrik"
            SSH_HOSTKEY_B64="AAAAC3NzaC1lZDI1NTE5AAAAIHPBE14yMazZvl5EXXQ5tNdknDgXkKiWklLMq5Rgbdxb"
            SSH_HOSTKEY_SHA256="SHA256:um1NvH6IejGnU5qmfW3Pj8aH/XcSGeJR4PBdSl2GBP4"
            DEFAULT_ENROLL_TOKEN="8a9e8aa132e973fdc66ef80a99c980bf4dc49faee42acf1b91c4e9e9ea0c6837"
            ;;
        ws-1.acidfit.eu)
            REMOTE_PROFILE="ACID Fit"
            SSH_HOSTKEY_B64="AAAAC3NzaC1lZDI1NTE5AAAAIPMg8yWTEBnbfsIoBcMN3F+xk1nyjkUB+2N5EOCicuSS"
            SSH_HOSTKEY_SHA256="SHA256:69+df167CWRncDqS2kShz5Vu/g5HU7A0JCIOdpgUH7A"
            DEFAULT_ENROLL_TOKEN="8f81f2d6b705c4e0f460d209f97480f6594500a6784121525847d9ec3a319f50"
            ;;
        *)
            die "Fuer Remote-Server $REMOTE_HOST ist noch kein Remote-Profil im Universal-Installer hinterlegt."
            ;;
    esac
}


log()  { printf '[vmk-remote] %s\n' "$*"; }
warn() { printf '[vmk-remote] WARNUNG: %s\n' "$*" >&2; }
die()  { printf '[vmk-remote] FEHLER: %s\n' "$*" >&2; exit 1; }

need_root() {
    [ "$(id -u)" -eq 0 ] || die "Dieses Skript muss als root laufen."
}

have() { command -v "$1" >/dev/null 2>&1; }

install_dependencies() {
    local missing=()
    have ssh || missing+=(openssh-client)
    have ssh-keygen || missing+=(openssh-client)
    have curl || missing+=(curl)

    [ "${#missing[@]}" -eq 0 ] && return 0

    have apt-get || die "Fehlende Programme: ${missing[*]}. apt-get ist nicht vorhanden."

    log "Installiere fehlende Pakete: ${missing[*]}"
    export DEBIAN_FRONTEND=noninteractive
    apt-get update
    apt-get install -y --no-install-recommends "${missing[@]}"
}

read_station_config() {
    [ -r "$CONFIG_FILE" ] || die "Konfigurationsdatei fehlt: $CONFIG_FILE"

    STATION_NR="$(sed -nE 's/^[[:space:]]*set[[:space:]]+STATION_NR[[:space:]]+"?([0-9]+)"?.*$/\1/p' "$CONFIG_FILE" | head -n1)"
    WS_URL="$(sed -nE 's/^[[:space:]]*set[[:space:]]+WS_URL[[:space:]]+"([^"]+)".*$/\1/p' "$CONFIG_FILE" | head -n1)"

    [ -n "$STATION_NR" ] || die "STATION_NR konnte aus $CONFIG_FILE nicht gelesen werden."
    [ -n "$WS_URL" ] || die "WS_URL konnte aus $CONFIG_FILE nicht gelesen werden."

    case "$STATION_NR" in
        *[!0-9]*) die "Ungueltige STATION_NR: $STATION_NR" ;;
    esac
    [ "$STATION_NR" -ge 1 ] && [ "$STATION_NR" -le 65535 ] || die "STATION_NR muss zwischen 1 und 65535 liegen."

    REMOTE_HOST="$(printf '%s\n' "$WS_URL" | sed -E 's#^[A-Za-z][A-Za-z0-9+.-]*://([^/:]+).*#\1#')"
    [ -n "$REMOTE_HOST" ] || die "Servername konnte aus WS_URL nicht ermittelt werden: $WS_URL"
    [ "$REMOTE_HOST" != "$WS_URL" ] || die "WS_URL hat kein erwartetes URL-Format: $WS_URL"

    select_remote_profile

    REMOTE_PORT="$STATION_NR"
    ENROLL_URL="https://${REMOTE_HOST}${ENROLL_PATH}"

    log "Client-Version: ${VERSION}"
    log "Server-Profil:  $REMOTE_PROFILE"
    log "Station:        $STATION_NR"
    log "Webservice:     $WS_URL"
    log "Remote-Server:  $REMOTE_HOST"
    log "Gateway-Port:   $SSH_PORT"
    log "Stations-Port:  $REMOTE_PORT"
}

prepare_directories() {
    install -d -m 0700 "$STATE_DIR"
    install -d -m 0700 "$ETC_DIR"
}

generate_key() {
    if [ ! -s "$KEY_FILE" ]; then
        log "Erzeuge individuellen Ed25519-Schluessel fuer Station $STATION_NR"
        ssh-keygen -q -t ed25519 -N '' -C "velometrik-remote-station-${STATION_NR}" -f "$KEY_FILE"
    else
        log "Vorhandener Remote-Schluessel wird weiterverwendet."
    fi

    chmod 0600 "$KEY_FILE"
    chmod 0644 "${KEY_FILE}.pub"
}

check_remote_ssh_port() {
    log "Pruefe SSH-Erreichbarkeit ${REMOTE_HOST}:${SSH_PORT}"

    # Use bash /dev/tcp so no netcat dependency is required.
    if ! timeout 8 bash -c "exec 3<>/dev/tcp/${REMOTE_HOST}/${SSH_PORT}" 2>/dev/null; then
        warn "TCP ${SSH_PORT} auf ${REMOTE_HOST} ist von dieser Station nicht erreichbar."
        warn "Pruefen: DNS, Firewall/NAT und ob der Remote-SSH-Gateway auf ${REMOTE_HOST}:${SSH_PORT} lauscht."
        die "Ohne erreichbaren SSH-Port kann der Remote-Tunnel nicht aufgebaut werden."
    fi

    log "SSH-Port ist erreichbar."
}

pin_server_host_key() {
    local tmp actual

    # Build the exact known_hosts entry from the verified gateway key.
    # This intentionally overwrites any entry created by older installer versions
    # (for example the former port-22 gateway key).
    tmp="$(mktemp)"
    printf '[%s]:%s %s %s\n' \
        "$REMOTE_HOST" "$SSH_PORT" "$SSH_HOSTKEY_TYPE" "$SSH_HOSTKEY_B64" >"$tmp"

    # Verify the embedded public key itself before installing it. This catches
    # accidental corruption of the installer/package.
    actual="$(ssh-keygen -lf "$tmp" -E sha256 2>/dev/null | awk 'NR==1 {print $2}')"
    [ "$actual" = "$SSH_HOSTKEY_SHA256" ] || {
        rm -f "$tmp"
        die "Eingebetteter Gateway-Hostkey ist ungueltig. Erwartet: $SSH_HOSTKEY_SHA256, erhalten: ${actual:-unbekannt}"
    }

    install -m 0600 "$tmp" "$KNOWN_HOSTS_FILE"
    rm -f "$tmp"
    log "SSH-Gateway-Hostkey fest gepinnt: $SSH_HOSTKEY_SHA256"
}

get_enroll_token() {
    # Zero-touch fleet rollout: the bootstrap credential is embedded in this
    # installer. An environment override is kept only for future token rotation.
    ENROLL_TOKEN="${VMK_REMOTE_ENROLL_TOKEN:-$DEFAULT_ENROLL_TOKEN}"
}

enroll_key() {
    local pub hostname response http_body status

    # If this exact key/station was already enrolled, do not need the bootstrap token again.
    local current_fingerprint
    current_fingerprint="$(ssh-keygen -lf "${KEY_FILE}.pub" -E sha256 | awk '{print $2}')"
    if [ -s "$ENROLLED_MARKER" ] \
       && grep -qx "station=${STATION_NR}" "$ENROLLED_MARKER" \
       && grep -qx "host=${REMOTE_HOST}" "$ENROLLED_MARKER" \
       && grep -qx "key_fingerprint=${current_fingerprint}" "$ENROLLED_MARKER"; then
        log "Station ist mit diesem Schluessel bereits registriert."
        return 0
    fi

    get_enroll_token
    [ -n "$ENROLL_TOKEN" ] || die "Kein Enrollment-Token verfuegbar."

    pub="$(cat "${KEY_FILE}.pub")"
    hostname="$(hostname 2>/dev/null || printf 'station-%s' "$STATION_NR")"

    log "Registriere Station $STATION_NR automatisch bei $ENROLL_URL"

    response="$(mktemp)"
    status="$(curl --silent --show-error \
        --connect-timeout 10 \
        --max-time 30 \
        --output "$response" \
        --write-out '%{http_code}' \
        --request POST \
        --header "Authorization: Bearer ${ENROLL_TOKEN}" \
        --header 'Accept: text/plain' \
        --data-urlencode "station=${STATION_NR}" \
        --data-urlencode "public_key=${pub}" \
        --data-urlencode "hostname=${hostname}" \
        --data-urlencode "remote_port=${REMOTE_PORT}" \
        --data-urlencode "client_version=${VERSION}" \
        "$ENROLL_URL")" || {
            rm -f "$response"
            die "Enrollment-Anfrage konnte nicht ausgefuehrt werden."
        }

    http_body="$(cat "$response")"
    rm -f "$response"

    [ "$status" = "200" ] || die "Enrollment fehlgeschlagen (HTTP $status): $http_body"
    printf '%s\n' "$http_body" | grep -q '^OK\([[:space:]].*\)\?$' || die "Enrollment-Server hat keine gueltige OK-Antwort geliefert: $http_body"

    {
        printf 'station=%s\n' "$STATION_NR"
        printf 'host=%s\n' "$REMOTE_HOST"
        printf 'key_fingerprint=%s\n' "$(ssh-keygen -lf "${KEY_FILE}.pub" -E sha256 | awk '{print $2}')"
        printf 'registered_at=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
    } > "$ENROLLED_MARKER"
    chmod 0600 "$ENROLLED_MARKER"

    ENROLL_TOKEN=""
    unset VMK_REMOTE_ENROLL_TOKEN || true

    log "Registrierung erfolgreich."
}

write_client_conf() {
    cat >"$CLIENT_CONF" <<EOF_CONF
# Generated by Velometrik Remote Client Installer $VERSION
REMOTE_HOST='$REMOTE_HOST'
REMOTE_USER='$REMOTE_USER'
SSH_PORT='$SSH_PORT'
REMOTE_BIND_ADDRESS='$REMOTE_BIND_ADDRESS'
REMOTE_PORT='$REMOTE_PORT'
LOCAL_SSH_HOST='$LOCAL_SSH_HOST'
LOCAL_SSH_PORT='$LOCAL_SSH_PORT'
KEY_FILE='$KEY_FILE'
KNOWN_HOSTS_FILE='$KNOWN_HOSTS_FILE'
EOF_CONF
    chmod 0600 "$CLIENT_CONF"
}

write_runner() {
    cat >"$RUNNER" <<'EOF_RUNNER'
#!/bin/bash
set -Eeuo pipefail

CONF='/etc/velometrik/remote/client.conf'
[ -r "$CONF" ] || { echo "vmk-remote: missing $CONF" >&2; exit 1; }
# shellcheck disable=SC1090
. "$CONF"

exec /usr/bin/ssh \
    -N \
    -T \
    -p "$SSH_PORT" \
    -i "$KEY_FILE" \
    -o BatchMode=yes \
    -o PasswordAuthentication=no \
    -o ChallengeResponseAuthentication=no \
    -o IdentitiesOnly=yes \
    -o HostKeyAlgorithms=ssh-ed25519 \
    -o UserKnownHostsFile="$KNOWN_HOSTS_FILE" \
    -o GlobalKnownHostsFile=/dev/null \
    -o StrictHostKeyChecking=yes \
    -o ExitOnForwardFailure=yes \
    -o ServerAliveInterval=5 \
    -o ServerAliveCountMax=1 \
    -o ConnectTimeout=8 \
    -o ConnectionAttempts=1 \
    -o TCPKeepAlive=yes \
    -o LogLevel=ERROR \
    -R "${REMOTE_BIND_ADDRESS}:${REMOTE_PORT}:${LOCAL_SSH_HOST}:${LOCAL_SSH_PORT}" \
    "${REMOTE_USER}@${REMOTE_HOST}"
EOF_RUNNER
    chmod 0755 "$RUNNER"
}

write_systemd_unit() {
    cat >"$UNIT_FILE" <<EOF_UNIT
[Unit]
Description=Velometrik Remote SSH Tunnel
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
ExecStart=$RUNNER
Restart=always
RestartSec=1
TimeoutStopSec=5

[Install]
WantedBy=multi-user.target
EOF_UNIT
    chmod 0644 "$UNIT_FILE"
}

check_local_sshd() {
    if have ss && ! ss -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)22$'; then
        warn "Auf TCP-Port 22 wurde lokal kein SSH-Server erkannt. Der Tunnel kann trotzdem starten, Remote-Login wird aber erst funktionieren, sobald sshd auf Port 22 lauscht."
    fi
}

start_service() {
    systemctl daemon-reload
    systemctl enable "$SERVICE" >/dev/null
    systemctl restart "$SERVICE"

    sleep 2

    if systemctl is-active --quiet "$SERVICE"; then
        log "Dienst laeuft."
    else
        warn "Dienst ist noch nicht aktiv. Letzte Meldungen:"
        journalctl -u "$SERVICE" -n 25 --no-pager || true
        return 1
    fi
}

show_status() {
    printf '\n'
    log "Installation abgeschlossen."
    log "Gateway: ${REMOTE_HOST}:${SSH_PORT}"
    log "Tunnel:  ${REMOTE_BIND_ADDRESS}:${REMOTE_PORT} -> Station ${STATION_NR} ${LOCAL_SSH_HOST}:${LOCAL_SSH_PORT}"
    systemctl --no-pager --full status "$SERVICE" 2>/dev/null | sed -n '1,12p' || true
    printf '\n'
    log "Logs:   journalctl -u $SERVICE -f"
    log "Status: systemctl status $SERVICE"
}

uninstall_client() {
    log "Entferne Velometrik Remote Client."
    systemctl disable --now "$SERVICE" >/dev/null 2>&1 || true
    rm -f "$UNIT_FILE" "$RUNNER" "$CLIENT_CONF" "$KNOWN_HOSTS_FILE" "$ENROLLED_MARKER"
    systemctl daemon-reload
    if [ "${1:-}" = "--purge-key" ]; then
        rm -f "$KEY_FILE" "${KEY_FILE}.pub"
        rmdir "$STATE_DIR" 2>/dev/null || true
        log "Individueller Stationsschluessel wurde ebenfalls entfernt."
    else
        log "Stationsschluessel bleibt erhalten: $KEY_FILE"
    fi
}

main() {
    need_root
    log "Installer-Version: $VERSION"
    log "Enrollment:      Zero-Touch (Bootstrap-Token eingebettet)"

    case "${1:-}" in
        --uninstall)
            uninstall_client "${2:-}"
            exit 0
            ;;
        --help|-h)
            cat <<EOF_HELP
Velometrik Remote Client Installer $VERSION

Aufruf:
  $0                Installieren / aktualisieren
  $0 --uninstall    Dienst entfernen, Stationsschluessel behalten
  $0 --uninstall --purge-key
                    Dienst und Stationsschluessel entfernen

Optionale Umgebungsvariablen:
  VMK_REMOTE_ENROLL_TOKEN   (Override fuer Token-Rotation)
  VMK_REMOTE_ENROLL_PATH

Erstregistrierung:
  Vollautomatisch; kein Token-File und keine Eingabe erforderlich.

Gateway:
  Server wird aus WS_URL ermittelt; das passende Server-Profil wird automatisch gewaehlt.
  SSH-Port 443, serverbezogener Ed25519-Hostkey fest im Installer gepinnt.
EOF_HELP
            exit 0
            ;;
    esac

    install_dependencies
    read_station_config
    prepare_directories
    generate_key
    check_remote_ssh_port
    pin_server_host_key
    enroll_key
    write_client_conf
    write_runner
    write_systemd_unit
    check_local_sshd
    start_service
    show_status
}

main "$@"
