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

SERVICE_NAME="${VELORAMESH_SERVICE_NAME:-veloramesh-server}"
EDGE_SERVICE_NAME="${VELORAMESH_EDGE_SERVICE_NAME:-veloramesh-edge}"
SERVICE_USER="${VELORAMESH_SERVICE_USER:-veloramesh}"
SERVICE_GROUP="${VELORAMESH_SERVICE_GROUP:-veloramesh}"
INSTALL_DIR="${VELORAMESH_INSTALL_DIR:-/opt/veloramesh}"
CONFIG_DIR="${VELORAMESH_CONFIG_DIR:-/etc/veloramesh}"
DATA_DIR="${VELORAMESH_DATA_DIR:-/var/lib/veloramesh}"
ENV_FILE="${VELORAMESH_ENV_FILE:-$CONFIG_DIR/veloramesh.env}"
SERVER_BIN="${VELORAMESH_SERVER_BIN:-$INSTALL_DIR/veloramesh-server}"
EDGE_BIN="${VELORAMESH_EDGE_BIN:-$INSTALL_DIR/veloramesh-edge}"
TLS_DIR="$CONFIG_DIR/tls"
TLS_CERT="$TLS_DIR/fullchain.pem"
TLS_KEY="$TLS_DIR/privkey.pem"
DEFAULT_TLS_CERT_NAME="${VELORAMESH_TLS_CERT_NAME:-veloramesh.com}"
DEFAULT_API_BIND_ADDR="${VELORAMESH_API_BIND_ADDR:-:2053}"
DEFAULT_EDGE_BIND_ADDR="${VELORAMESH_EDGE_BIND_ADDR:-:3478}"
DEFAULT_VIRTUAL_IPV4_CIDR="${VELORAMESH_DEFAULT_VIRTUAL_IPV4_CIDR:-10.253.0.0/16}"
SYSTEMD_DIR="${VELORAMESH_SYSTEMD_DIR:-/etc/systemd/system}"
UNIT_FILE="$SYSTEMD_DIR/$SERVICE_NAME.service"
EDGE_UNIT_FILE="$SYSTEMD_DIR/$EDGE_SERVICE_NAME.service"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_SCRIPT="${VELORAMESH_INSTALL_SCRIPT:-$SCRIPT_DIR/install-veloramesh-server.sh}"
REQUESTED_ROLE="${VELORAMESH_SERVER_ROLE:-all}"

usage() {
    cat <<USAGE
Usage: sudo bash manage-veloramesh-server.sh [options]

Options:
  --menu        Open the interactive server management menu. This is the default.
  --setup       Run the guided install/update wizard.
  --fresh       Run the guided fresh install wizard. This deletes VeloraMesh data.
  --install     Install or update files using install-veloramesh-server.sh.
  --role <all|api|edge>
                Select all-in-one, API-only, or edge-only for --setup/--fresh/--install.
  --install-tls Install a validated TLS certificate/key pair and restart an active API service.
  --tls-cert <path>
                Certificate chain path required with --install-tls.
  --tls-key <path>
                Unencrypted private key path required with --install-tls.
  --approve-spki-pin <sha256/base64>
                Approve exactly this new SPKI pin for an intentional key rotation.
  --status      Show service and binary status.
  -h, --help    Show this help text.

This script is a local operator wrapper around:
  $SERVER_BIN admin --env $ENV_FILE

It does not expose any public management API.

Environment overrides:
  VELORAMESH_TLS_CERT_NAME defaults to veloramesh.com for Let's Encrypt path prompts.
  VELORAMESH_API_BIND_ADDR defaults to :2053 and is shown as API host/port prompts.
  VELORAMESH_EDGE_BIND_ADDR defaults to :3478 and is shown as edge host/port prompts.
  VELORAMESH_DEFAULT_VIRTUAL_IPV4_CIDR defaults to 10.253.0.0/16 for new networks.
USAGE
}

log() {
    printf '[veloramesh] %s\n' "$1" >&2
}

fail() {
    printf '[veloramesh] error: %s\n' "$1" >&2
    exit 1
}

need_command() {
    command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
}

normalize_role() {
    case "$1" in
        all|all-in-one|api-edge|api+edge)
            printf 'all'
            ;;
        api|api-only)
            printf 'api'
            ;;
        edge|edge-only)
            printf 'edge'
            ;;
        *)
            fail "unknown role: $1"
            ;;
    esac
}

role_label() {
    case "${1:-all}" in
        all) printf 'all-in-one API + Edge' ;;
        api) printf 'API-only' ;;
        edge) printf 'Edge-only' ;;
        none) printf 'no installed role' ;;
    esac
}

role_services() {
    case "${1:-all}" in
        all) printf '%s\n%s\n' "$SERVICE_NAME" "$EDGE_SERVICE_NAME" ;;
        api) printf '%s\n' "$SERVICE_NAME" ;;
        edge) printf '%s\n' "$EDGE_SERVICE_NAME" ;;
    esac
}

role_has_api() {
    case "${1:-all}" in
        all|api) return 0 ;;
        *) return 1 ;;
    esac
}

role_has_edge() {
    case "${1:-all}" in
        all|edge) return 0 ;;
        *) return 1 ;;
    esac
}

detected_service_role() {
    local has_api=0
    local has_edge=0

    if [ -f "$UNIT_FILE" ] || [ -x "$SERVER_BIN" ]; then
        has_api=1
    fi
    if [ -f "$EDGE_UNIT_FILE" ] || [ -x "$EDGE_BIN" ]; then
        has_edge=1
    fi

    if [ "$has_api" -eq 1 ] && [ "$has_edge" -eq 1 ]; then
        printf 'all'
    elif [ "$has_api" -eq 1 ]; then
        printf 'api'
    elif [ "$has_edge" -eq 1 ]; then
        printf 'edge'
    else
        printf 'none'
    fi
}

active_service_role() {
    local detected
    detected="$(detected_service_role)"
    if [ "$REQUESTED_ROLE" = "all" ]; then
        printf '%s' "$detected"
        return
    fi
    printf '%s' "$REQUESTED_ROLE"
}

require_root() {
    [ "$(id -u)" -eq 0 ] || fail "run this manager as root"
}

pause() {
    printf '\nPress Enter to continue... '
    read -r _ || true
}

prompt() {
    local label="$1"
    local value
    printf '%s: ' "$label" >&2
    read -r value
    printf '%s' "$value"
}

prompt_default() {
    local label="$1"
    local default="$2"
    local value
    if [ -n "$default" ]; then
        printf '%s [%s]: ' "$label" "$default" >&2
    else
        printf '%s: ' "$label" >&2
    fi
    read -r value
    if [ -z "$value" ]; then
        value="$default"
    fi
    printf '%s' "$value"
}

prompt_required() {
    local label="$1"
    local value
    value="$(prompt "$label")"
    if [ -z "$value" ]; then
        log "$label is required"
        return 1
    fi
    printf '%s' "$value"
}

prompt_secret() {
    local label="$1"
    local value
    printf '%s: ' "$label" >&2
    read -r -s value
    printf '\n' >&2
    printf '%s' "$value"
}

confirm_default_yes() {
    local message="$1"
    local answer
    printf '%s [Y/n]: ' "$message"
    read -r answer
    answer="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')"
    case "$answer" in
        ""|y|yes|e|evet) return 0 ;;
        n|no|h|hayir|hayır) return 1 ;;
        *) return 1 ;;
    esac
}

confirm_yes() {
    local message="$1"
    local answer
    printf '%s Type yes to continue: ' "$message"
    read -r answer
    [ "$answer" = "yes" ]
}

confirm_phrase() {
    local message="$1"
    local phrase="$2"
    local answer
    printf '%s Type %s to continue: ' "$message" "$phrase"
    read -r answer
    [ "$answer" = "$phrase" ]
}

read_env_value() {
    local wanted="$1"
    local key
    local value
    [ -f "$ENV_FILE" ] || return 0
    while IFS='=' read -r key value; do
        case "$key" in
            ""|\#*) continue ;;
        esac
        if [ "$key" = "$wanted" ]; then
            printf '%s' "$value"
            return 0
        fi
    done < "$ENV_FILE"
    return 0
}

configured_or_default() {
    local key="$1"
    local default="$2"
    local value
    value="$(read_env_value "$key")"
    if [ -n "$value" ]; then
        printf '%s' "$value"
    else
        printf '%s' "$default"
    fi
}

is_ipv4_literal() {
    local value="$1"
    local a
    local b
    local c
    local d
    local extra
    IFS=. read -r a b c d extra <<< "$value"
    [ -z "${extra:-}" ] || return 1
    for octet in "$a" "$b" "$c" "$d"; do
        case "$octet" in
            ""|*[!0-9]*) return 1 ;;
        esac
        [ "$octet" -ge 0 ] && [ "$octet" -le 255 ] || return 1
    done
    return 0
}

validate_listen_addr_value() {
    local value="$1"
    local host
    local port
    [ -n "$value" ] || return 1
    case "$value" in
        *[[:space:]]*) return 1 ;;
        *://*|*/*|*\?*|*\#*) return 1 ;;
        *:*) ;;
        *) return 1 ;;
    esac
    port="${value##*:}"
    case "$port" in
        ""|*[!0-9]*) return 1 ;;
    esac
    [ "$port" -ge 1 ] && [ "$port" -le 65535 ] || return 1
    host="${value%:*}"
    case "$host" in
        ""|"localhost") return 0 ;;
        \[*\])
            host="${host#\[}"
            host="${host%\]}"
            case "$host" in
                *[!0-9A-Fa-f:.]*) return 1 ;;
            esac
            return 0
            ;;
        *:*) return 1 ;;
        *[!0-9.]*) return 1 ;;
    esac
    is_ipv4_literal "$host"
}

listen_host_from_addr() {
    local value="$1"
    local host
    case "$value" in
        \[*\]:*)
            host="${value%%]:*}"
            host="${host#\[}"
            printf '%s' "$host"
            ;;
        :*)
            printf ''
            ;;
        *:*)
            printf '%s' "${value%:*}"
            ;;
        *)
            printf ''
            ;;
    esac
}

listen_port_from_addr() {
    local value="$1"
    printf '%s' "${value##*:}"
}

normalize_bind_host() {
    local value="$1"
    value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
    case "$value" in
        ""|"all"|"any"|"*"|"0.0.0.0")
            printf ''
            ;;
        "localhost")
            printf 'localhost'
            ;;
        *)
            printf '%s' "$1"
            ;;
    esac
}

validate_bind_host_value() {
    local value="$1"
    [ -n "$value" ] || return 0
    case "$value" in
        *[[:space:]]*) return 1 ;;
        *://*|*/*|*\?*|*\#*) return 1 ;;
        "localhost") return 0 ;;
        \[*\])
            value="${value#\[}"
            value="${value%\]}"
            case "$value" in
                ""|*[!0-9A-Fa-f:.]*) return 1 ;;
            esac
            return 0
            ;;
        *:*)
            case "$value" in
                ""|*[!0-9A-Fa-f:.]*) return 1 ;;
            esac
            return 0
            ;;
        *[!0-9.]*) return 1 ;;
    esac
    is_ipv4_literal "$value"
}

validate_bind_port_value() {
    local value="$1"
    case "$value" in
        ""|*[!0-9]*) return 1 ;;
    esac
    [ "$value" -ge 1 ] && [ "$value" -le 65535 ]
}

validate_virtual_ipv4_cidr_value() {
    local value="$1"
    local ip
    local bits
    local a
    local b
    local c
    local d
    local extra
    case "$value" in
        */*) ;;
        *) return 1 ;;
    esac
    ip="${value%/*}"
    bits="${value##*/}"
    case "$bits" in
        ""|*[!0-9]*) return 1 ;;
    esac
    [ "$bits" -ge 16 ] && [ "$bits" -le 24 ] || return 1
    IFS=. read -r a b c d extra <<< "$ip"
    [ -z "${extra:-}" ] || return 1
    for octet in "$a" "$b" "$c" "$d"; do
        case "$octet" in
            ""|*[!0-9]*) return 1 ;;
        esac
        [ "$octet" -ge 0 ] && [ "$octet" -le 255 ] || return 1
    done
    if [ "$a" -eq 10 ]; then
        return 0
    fi
    [ "$a" -eq 172 ] && [ "$b" -ge 16 ] && [ "$b" -le 31 ]
}

prompt_virtual_ipv4_cidr() {
    local label="$1"
    local default="$2"
    local value
    while true; do
        value="$(prompt_default "$label" "$default")"
        if validate_virtual_ipv4_cidr_value "$value"; then
            printf '%s' "$value"
            return 0
        fi
        log "$label must be a private CIDR inside 10.0.0.0/8 or 172.16.0.0/12 with prefix /16-/24"
        log "avoid CGNAT 100.64.0.0/10, common home LAN 192.168.0.0/16, and public ranges"
    done
}

virtual_ipv4_member_capacity() {
    local cidr="$1"
    local bits="${cidr##*/}"
    local capacity=$(( (1 << (32 - bits)) - 2 ))
    if [ "$capacity" -gt 255 ]; then
        capacity=255
    fi
    printf '%s' "$capacity"
}

prompt_network_member_limit() {
    local cidr="$1"
    local maximum
    local default=255
    local value
    maximum="$(virtual_ipv4_member_capacity "$cidr")"
    if [ "$default" -gt "$maximum" ]; then
        default="$maximum"
    fi
    while true; do
        value="$(prompt_default "Network member limit (1-$maximum)" "$default")"
        case "$value" in
            ''|*[!0-9]*)
                log "network member limit must be a number between 1 and $maximum"
                ;;
            *)
                if [ "$value" -ge 1 ] && [ "$value" -le "$maximum" ]; then
                    printf '%s' "$value"
                    return 0
                fi
                log "network member limit must be a number between 1 and $maximum"
                ;;
        esac
    done
}

join_listen_addr() {
    local host="$1"
    local port="$2"
    if [ -z "$host" ]; then
        printf ':%s' "$port"
        return 0
    fi
    case "$host" in
        \[*\]) printf '%s:%s' "$host" "$port" ;;
        *:*) printf '[%s]:%s' "$host" "$port" ;;
        *) printf '%s:%s' "$host" "$port" ;;
    esac
}

prompt_bind_host() {
    local label="$1"
    local default="$2"
    local display_default="$default"
    local value
    [ -n "$display_default" ] || display_default="all interfaces"
    while true; do
        printf '%s [%s]: ' "$label" "$display_default" >&2
        read -r value
        if [ -z "$value" ]; then
            value="$default"
        fi
        value="$(normalize_bind_host "$value")"
        if validate_bind_host_value "$value"; then
            printf '%s' "$value"
            return 0
        fi
        printf '[veloramesh] %s must be blank/all, localhost, or a local IP address. Public DNS names belong in client setup.\n' "$label" >&2
    done
}

prompt_bind_port() {
    local label="$1"
    local default="$2"
    local value
    while true; do
        value="$(prompt_default "$label" "$default")"
        if validate_bind_port_value "$value"; then
            printf '%s' "$value"
            return 0
        fi
        printf '[veloramesh] %s must be a TCP/UDP port between 1 and 65535.\n' "$label" >&2
    done
}

prompt_listen_addr_parts() {
    local label="$1"
    local protocol="$2"
    local default_addr="$3"
    local default_host
    local default_port
    local host
    local port
    local addr
    default_host="$(listen_host_from_addr "$default_addr")"
    default_port="$(listen_port_from_addr "$default_addr")"
    while true; do
        host="$(prompt_bind_host "$label bind host (blank/all for all interfaces)" "$default_host")"
        port="$(prompt_bind_port "$label $protocol port" "$default_port")"
        addr="$(join_listen_addr "$host" "$port")"
        if validate_listen_addr_value "$addr"; then
            printf '%s' "$addr"
            return 0
        fi
        printf '[veloramesh] %s listener could not be normalized. Try another local host/port pair.\n' "$label" >&2
    done
}

admin_cmd() {
    [ -x "$SERVER_BIN" ] || {
        log "server binary not found: $SERVER_BIN"
        log "install or update the server first"
        return 1
    }
    [ -f "$ENV_FILE" ] || {
        log "environment file not found: $ENV_FILE"
        log "install the server first"
        return 1
    }
    "$SERVER_BIN" admin --env "$ENV_FILE" "$@"
}

ensure_safe_data_dir() {
    case "$DATA_DIR" in
        ""|"/"|"/etc"|"/opt"|"/var"|"/var/lib"|"/usr"|"/usr/local")
            fail "refusing unsafe data directory: $DATA_DIR"
            ;;
    esac
    case "$DATA_DIR" in
        *veloramesh*|*VeloraMesh*) ;;
        *) fail "refusing non-VeloraMesh data directory: $DATA_DIR" ;;
    esac
}

repair_data_permissions() {
    [ -d "$DATA_DIR" ] || return 0
    id -u "$SERVICE_USER" >/dev/null 2>&1 || return 0
    getent group "$SERVICE_GROUP" >/dev/null 2>&1 || return 0

    ensure_safe_data_dir
    chown -R "$SERVICE_USER:$SERVICE_GROUP" "$DATA_DIR"
    chmod 0750 "$DATA_DIR"
    find "$DATA_DIR" -type d -exec chmod 0750 {} +
    find "$DATA_DIR" -maxdepth 1 -type f \( -name 'veloramesh.db' -o -name 'veloramesh.db-wal' -o -name 'veloramesh.db-shm' -o -name 'membership-ticket.key' -o -name 'edge-ticket.key' \) -exec chmod 0640 {} +
}

admin_safe() {
    if ! admin_cmd "$@"; then
        log "admin command failed"
        return 1
    fi
    repair_data_permissions
}

run_install() {
    local mode="${1:-start}"
    local role="${2:-all}"
    local edge_key_source="${3:-}"
    local api_bind_addr="${4:-}"
    local edge_bind_addr="${5:-}"
    local default_virtual_ipv4_cidr="${6:-}"
    local args=()
    [ -f "$INSTALL_SCRIPT" ] || fail "install script not found: $INSTALL_SCRIPT"
    role="$(normalize_role "$role")"
    args+=(--role "$role")
    if [ "$mode" = "no-start" ]; then
        args+=(--no-start)
    fi
    if [ -n "$api_bind_addr" ]; then
        args+=(--api-bind "$api_bind_addr")
    fi
    if [ -n "$edge_bind_addr" ]; then
        args+=(--edge-bind "$edge_bind_addr")
    fi
    if [ -n "$default_virtual_ipv4_cidr" ]; then
        args+=(--default-virtual-ipv4-cidr "$default_virtual_ipv4_cidr")
    fi
    if [ -n "$edge_key_source" ]; then
        args+=(--edge-ticket-key "$edge_key_source")
    fi
    if ! bash "$INSTALL_SCRIPT" "${args[@]}"; then
        log "install or update failed"
        return 1
    fi
}

certificate_public_key_digest() {
    local cert_path="$1"
    openssl x509 -in "$cert_path" -pubkey -noout 2>/dev/null |
        openssl pkey -pubin -outform DER 2>/dev/null |
        openssl dgst -sha256 2>/dev/null
}

private_key_public_digest() {
    local key_path="$1"
    openssl pkey -in "$key_path" -passin pass: -pubout -outform DER 2>/dev/null |
        openssl dgst -sha256 2>/dev/null
}

certificate_spki_pin() {
    local cert_path="$1"
    local digest
    digest="$(
        openssl x509 -in "$cert_path" -pubkey -noout 2>/dev/null |
            openssl pkey -pubin -outform DER 2>/dev/null |
            openssl dgst -sha256 -binary 2>/dev/null |
            openssl base64 -A 2>/dev/null
    )" || return 1
    [ -n "$digest" ] || return 1
    printf 'sha256/%s' "$digest"
}

validate_tls_pair() {
    local cert_path="$1"
    local key_path="$2"
    local cert_digest
    local key_digest
    local not_before
    local now

    if ! openssl x509 -in "$cert_path" -noout >/dev/null 2>&1; then
        log "TLS certificate is not a valid PEM X.509 certificate: $cert_path"
        return 1
    fi
    if ! openssl x509 -in "$cert_path" -checkend 0 -noout >/dev/null 2>&1; then
        log "TLS certificate is expired: $cert_path"
        return 1
    fi
    # OpenSSL checkend validates notAfter only; also reject future notBefore dates.
    not_before="$(openssl x509 -in "$cert_path" -startdate -noout)" || return 1
    not_before="$(LC_ALL=C date -u -d "${not_before#notBefore=}" +%s)" || return 1
    now="$(date -u +%s)" || return 1
    if [ "$not_before" -gt "$now" ]; then
        log "TLS certificate is not yet valid: $cert_path"
        return 1
    fi
    if ! openssl pkey -in "$key_path" -passin pass: -noout >/dev/null 2>&1; then
        log "TLS private key is invalid or requires a passphrase: $key_path"
        return 1
    fi
    cert_digest="$(certificate_public_key_digest "$cert_path")" || return 1
    key_digest="$(private_key_public_digest "$key_path")" || return 1
    if [ -z "$cert_digest" ] || [ "$cert_digest" != "$key_digest" ]; then
        log "TLS certificate and private key do not contain the same public key"
        return 1
    fi
}

install_tls_pair_atomically() {
    local cert_src="$1"
    local key_src="$2"
    local approved_spki_pin="${3:-}"
    local token="$$.$RANDOM"
    local cert_staged="$TLS_DIR/.fullchain.pem.new.$token"
    local key_staged="$TLS_DIR/.privkey.pem.new.$token"
    local cert_previous="$TLS_DIR/.fullchain.pem.previous.$token"
    local key_previous="$TLS_DIR/.privkey.pem.previous.$token"
    local cert_existed=0
    local key_existed=0
    local current_pin=""
    local next_pin

    need_command openssl
    [ -f "$cert_src" ] || { log "TLS certificate not found: $cert_src"; return 1; }
    [ -f "$key_src" ] || { log "TLS private key not found: $key_src"; return 1; }
    install -d -o root -g "$SERVICE_GROUP" -m 0750 "$TLS_DIR"

    if ! install -o root -g "$SERVICE_GROUP" -m 0644 "$cert_src" "$cert_staged"; then
        rm -f -- "$cert_staged" "$key_staged"
        return 1
    fi
    if ! install -o root -g "$SERVICE_GROUP" -m 0640 "$key_src" "$key_staged"; then
        rm -f -- "$cert_staged" "$key_staged"
        return 1
    fi
    if ! validate_tls_pair "$cert_staged" "$key_staged"; then
        rm -f -- "$cert_staged" "$key_staged"
        return 1
    fi

    next_pin="$(certificate_spki_pin "$cert_staged")" || {
        rm -f -- "$cert_staged" "$key_staged"
        return 1
    }
    if [ -f "$TLS_CERT" ]; then
        current_pin="$(certificate_spki_pin "$TLS_CERT" || true)"
    fi
    if [ -n "$current_pin" ] && [ "$current_pin" != "$next_pin" ]; then
        if [ "$approved_spki_pin" != "ROTATE" ] && [ "$approved_spki_pin" != "$next_pin" ]; then
            log "TLS public key rotation requires explicit operator approval"
            log "current_spki_pin=$current_pin"
            log "next_spki_pin=$next_pin"
            rm -f -- "$cert_staged" "$key_staged"
            return 2
        fi
    fi

    if [ -f "$TLS_CERT" ]; then
        if ! cp -p -- "$TLS_CERT" "$cert_previous"; then
            rm -f -- "$cert_staged" "$key_staged" "$cert_previous" "$key_previous"
            return 1
        fi
        cert_existed=1
    fi
    if [ -f "$TLS_KEY" ]; then
        if ! cp -p -- "$TLS_KEY" "$key_previous"; then
            rm -f -- "$cert_staged" "$key_staged" "$cert_previous" "$key_previous"
            return 1
        fi
        key_existed=1
    fi

    if ! mv -f -- "$cert_staged" "$TLS_CERT"; then
        rm -f -- "$cert_staged" "$key_staged" "$cert_previous" "$key_previous"
        return 1
    fi
    if ! mv -f -- "$key_staged" "$TLS_KEY"; then
        if [ "$cert_existed" -eq 1 ]; then
            mv -f -- "$cert_previous" "$TLS_CERT"
        else
            rm -f -- "$TLS_CERT"
        fi
        if [ "$key_existed" -eq 1 ] && [ -f "$key_previous" ]; then
            mv -f -- "$key_previous" "$TLS_KEY"
        fi
        rm -f -- "$cert_staged" "$key_staged" "$cert_previous" "$key_previous"
        return 1
    fi

    rm -f -- "$cert_previous" "$key_previous"
    log "installed validated TLS files"
    log "control_plane_spki_pin=$next_pin"
}

install_tls_files_interactive() {
    local cert_name="${1:-$DEFAULT_TLS_CERT_NAME}"
    local default_cert=""
    local default_key=""
    local cert_src
    local key_src
    local status
    if [ -n "$cert_name" ]; then
        default_cert="/etc/letsencrypt/live/$cert_name/fullchain.pem"
        default_key="/etc/letsencrypt/live/$cert_name/privkey.pem"
    fi

    while true; do
        cert_src="$(prompt_default "TLS fullchain path" "$default_cert")"
        key_src="$(prompt_default "TLS private key path" "$default_key")"
        status=0
        install_tls_pair_atomically "$cert_src" "$key_src" "" || status=$?
        case "$status" in
            0) return 0 ;;
            2)
                if confirm_phrase "This changes the control-plane SPKI identity. Pinned clients must trust the new fingerprint before or during this rotation." "ROTATE"; then
                    install_tls_pair_atomically "$cert_src" "$key_src" ROTATE
                    return
                fi
                log "TLS key rotation cancelled; existing TLS files were preserved"
                return 1
                ;;
        esac
        if ! confirm_default_yes "Try another TLS path?"; then
            return 1
        fi
    done
}

install_tls_files_non_interactive() {
    local cert_src="$1"
    local key_src="$2"
    local approved_spki_pin="${3:-}"
    install_tls_pair_atomically "$cert_src" "$key_src" "$approved_spki_pin"
    if systemctl is-active --quiet "$SERVICE_NAME"; then
        systemctl restart "$SERVICE_NAME"
        log "restarted API service after TLS renewal"
    fi
}

update_tls_files_interactive() {
    local cert_name
    cert_name="$(prompt_default "TLS certificate name for default Let's Encrypt paths" "$DEFAULT_TLS_CERT_NAME")"
    install_tls_files_interactive "$cert_name" || return
    if systemctl is-active --quiet "$SERVICE_NAME" && confirm_default_yes "Restart the API service now to activate the certificate?"; then
        systemctl restart "$SERVICE_NAME"
        systemctl --no-pager --full status "$SERVICE_NAME" || true
    fi
}

start_or_restart_service() {
    local role="${1:-all}"
    local service
    role="$(normalize_role "$role")"
    while IFS= read -r service; do
        [ -n "$service" ] || continue
        if systemctl is-active --quiet "$service"; then
            systemctl restart "$service"
        else
            systemctl enable --now "$service"
        fi
        systemctl --no-pager --full status "$service" || true
    done <<SERVICES
$(role_services "$role")
SERVICES
}

create_network_if_requested() {
    if confirm_default_yes "Create a network now?"; then
        while true; do
            if create_network; then
                return 0
            fi
            if ! confirm_default_yes "Network was not created. Try again?"; then
                log "continuing without creating a network"
                return 1
            fi
        done
    fi
}

guided_install_update() {
    local role="${1:-all}"
    local api_bind_addr=""
    local edge_bind_addr=""
    local edge_key_source=""
    local default_virtual_ipv4_cidr=""
    local tls_cert_name=""
    role="$(normalize_role "$role")"
    log "guided $(role_label "$role") install/update"
    log "bind host/port prompts are local listener values; use public API/edge domains only in client installers"

    if [ "$role" != "edge" ]; then
        api_bind_addr="$(prompt_listen_addr_parts "Local API" "TCP" "$(configured_or_default VELORAMESH_API_BIND_ADDR "$DEFAULT_API_BIND_ADDR")")"
    fi
    if [ "$role" != "api" ]; then
        edge_bind_addr="$(prompt_listen_addr_parts "Local edge discovery" "UDP" "$(configured_or_default VELORAMESH_EDGE_BIND_ADDR "$DEFAULT_EDGE_BIND_ADDR")")"
    fi
    if [ "$role" != "edge" ]; then
        default_virtual_ipv4_cidr="$(prompt_virtual_ipv4_cidr "Default virtual IPv4 CIDR for new networks" "$(configured_or_default VELORAMESH_DEFAULT_VIRTUAL_IPV4_CIDR "$DEFAULT_VIRTUAL_IPV4_CIDR")")"
    fi

    if [ "$role" = "edge" ]; then
        edge_key_source="$(prompt_default "Edge ticket key source path (blank if already installed on this host)" "")"
    fi

    run_install no-start "$role" "$edge_key_source" "$api_bind_addr" "$edge_bind_addr" "$default_virtual_ipv4_cidr" || return

    if [ "$role" = "edge" ] && [ ! -f "$DATA_DIR/edge-ticket.key" ]; then
        log "edge-ticket.key is required before Edge-only can start"
        log "copy it from the API host to $DATA_DIR/edge-ticket.key or rerun Edge-only install/update with a source path"
        server_self_test "$role"
        return
    fi

    if [ "$role" != "edge" ] && confirm_default_yes "Install TLS files now?"; then
        tls_cert_name="$(prompt_default "TLS certificate name for default Let's Encrypt paths" "$DEFAULT_TLS_CERT_NAME")"
        install_tls_files_interactive "$tls_cert_name" || return
    fi

    if [ "$role" != "edge" ]; then
        create_network_if_requested
    fi

    if confirm_default_yes "Start or restart $(role_label "$role") services now?"; then
        start_or_restart_service "$role"
    else
        log "$(role_label "$role") services not started"
    fi

    server_self_test "$role"
    log "guided setup complete"
}

show_status() {
    local role
    role="$(active_service_role)"

    if [ "$role" = "none" ]; then
        log "no VeloraMesh systemd unit or binary is installed on this host"
        return
    fi

    log "service_role=$(role_label "$role")"
    if role_has_api "$role"; then
        if [ -x "$SERVER_BIN" ]; then
            "$SERVER_BIN" version || true
        else
            log "API binary missing: $SERVER_BIN"
        fi
        if [ -f "$UNIT_FILE" ]; then
            systemctl --no-pager --full status "$SERVICE_NAME" || true
        else
            log "API service unit not installed on this host"
        fi
    fi

    if role_has_edge "$role"; then
        if [ -x "$EDGE_BIN" ]; then
            "$EDGE_BIN" version || true
        else
            log "edge binary missing: $EDGE_BIN"
        fi
        if [ -f "$EDGE_UNIT_FILE" ]; then
            systemctl --no-pager --full status "$EDGE_SERVICE_NAME" || true
        else
            log "edge service unit not installed on this host"
        fi
    fi
}

show_logs() {
    local role
    role="$(active_service_role)"

    if [ "$role" = "none" ]; then
        log "no VeloraMesh systemd unit or binary is installed on this host"
        return
    fi

    if role_has_api "$role"; then
        if [ -f "$UNIT_FILE" ]; then
            journalctl -u "$SERVICE_NAME" -n 120 --no-pager || true
        else
            log "API service unit not installed on this host"
        fi
    fi
    if role_has_edge "$role"; then
        if [ -f "$EDGE_UNIT_FILE" ]; then
            journalctl -u "$EDGE_SERVICE_NAME" -n 120 --no-pager || true
        else
            log "edge service unit not installed on this host"
        fi
    fi
}

server_self_test() {
    local role="${1:-all}"
    local api_bind_addr
    local edge_bind_addr
    role="$(normalize_role "$role")"
    log "running server self-test"
    if [ "$role" != "edge" ] && [ -x "$SERVER_BIN" ]; then
        "$SERVER_BIN" version || log "server binary version check failed"
    elif [ "$role" != "edge" ]; then
        log "server binary missing: $SERVER_BIN"
    fi
    if [ -f "$ENV_FILE" ]; then
        log "env_file=present"
        api_bind_addr="$(read_env_value VELORAMESH_API_BIND_ADDR)"
        edge_bind_addr="$(read_env_value VELORAMESH_EDGE_BIND_ADDR)"
        [ "$role" != "edge" ] && [ -n "$api_bind_addr" ] && log "api_bind_addr=$api_bind_addr"
        [ "$role" != "api" ] && [ -n "$edge_bind_addr" ] && log "edge_bind_addr=$edge_bind_addr"
    else
        log "env_file=missing"
    fi
    if [ "$role" != "edge" ] && [ -f "$UNIT_FILE" ]; then
        log "systemd_unit=present"
    elif [ "$role" != "edge" ]; then
        log "systemd_unit=missing"
    fi
    if [ "$role" != "api" ] && [ -x "$EDGE_BIN" ]; then
        log "edge_binary=present"
    elif [ "$role" != "api" ]; then
        log "edge_binary=missing"
    fi
    if [ "$role" != "api" ] && [ -f "$EDGE_UNIT_FILE" ]; then
        log "edge_systemd_unit=present"
    elif [ "$role" != "api" ]; then
        log "edge_systemd_unit=missing"
    fi
    if [ "$role" != "edge" ] && systemctl is-active --quiet "$SERVICE_NAME"; then
        log "service=active"
    elif [ "$role" != "edge" ]; then
        log "service=inactive"
    fi
    if [ "$role" != "api" ] && systemctl is-active --quiet "$EDGE_SERVICE_NAME"; then
        log "edge_service=active"
    elif [ "$role" != "api" ]; then
        log "edge_service=inactive"
    fi
}

service_action() {
    local action="$1"
    local did_work=0
    if [ -f "$UNIT_FILE" ]; then
        did_work=1
        systemctl "$action" "$SERVICE_NAME" || log "service $action failed"
        systemctl --no-pager --full status "$SERVICE_NAME" || true
    fi
    if [ -f "$EDGE_UNIT_FILE" ]; then
        did_work=1
        systemctl "$action" "$EDGE_SERVICE_NAME" || log "edge service $action failed"
        systemctl --no-pager --full status "$EDGE_SERVICE_NAME" || true
    fi
    if [ "$did_work" -eq 0 ]; then
        log "no VeloraMesh systemd unit is installed"
    fi
}

safe_remove_path() {
    local path="$1"
    case "$path" in
        ""|"/"|"/etc"|"/opt"|"/var"|"/var/lib"|"/usr"|"/usr/local")
            fail "refusing unsafe remove path: $path"
            ;;
    esac
    case "$path" in
        *veloramesh*|*VeloraMesh*) ;;
        *) fail "refusing to remove non-VeloraMesh path: $path" ;;
    esac
    if [ -e "$path" ]; then
        rm -rf -- "$path"
    fi
}

REMOVAL_API_WAS_ACTIVE=0
REMOVAL_API_WAS_ENABLED=0
REMOVAL_EDGE_WAS_ACTIVE=0
REMOVAL_EDGE_WAS_ENABLED=0

record_removal_service_state() {
    REMOVAL_API_WAS_ACTIVE=0
    REMOVAL_API_WAS_ENABLED=0
    REMOVAL_EDGE_WAS_ACTIVE=0
    REMOVAL_EDGE_WAS_ENABLED=0
    systemctl is-active --quiet "$SERVICE_NAME" && REMOVAL_API_WAS_ACTIVE=1
    systemctl is-enabled --quiet "$SERVICE_NAME" && REMOVAL_API_WAS_ENABLED=1
    systemctl is-active --quiet "$EDGE_SERVICE_NAME" && REMOVAL_EDGE_WAS_ACTIVE=1
    systemctl is-enabled --quiet "$EDGE_SERVICE_NAME" && REMOVAL_EDGE_WAS_ENABLED=1
    return 0
}

restore_removal_service_state() {
    if [ "$REMOVAL_API_WAS_ENABLED" -eq 1 ]; then
        systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 || log "recovery warning: could not re-enable API service"
    fi
    if [ "$REMOVAL_EDGE_WAS_ENABLED" -eq 1 ]; then
        systemctl enable "$EDGE_SERVICE_NAME" >/dev/null 2>&1 || log "recovery warning: could not re-enable edge service"
    fi
    if [ "$REMOVAL_API_WAS_ACTIVE" -eq 1 ]; then
        systemctl start "$SERVICE_NAME" >/dev/null 2>&1 || log "recovery warning: could not restart API service"
    fi
    if [ "$REMOVAL_EDGE_WAS_ACTIVE" -eq 1 ]; then
        systemctl start "$EDGE_SERVICE_NAME" >/dev/null 2>&1 || log "recovery warning: could not restart edge service"
    fi
}

quiesce_managed_service() {
    local service_name="$1"
    local unit_path="$2"
    if [ ! -f "$unit_path" ] && ! systemctl is-active --quiet "$service_name" && ! systemctl is-enabled --quiet "$service_name"; then
        return 0
    fi

    systemctl stop "$service_name" || return 1
    ! systemctl is-active --quiet "$service_name" || return 1
    systemctl disable "$service_name" || return 1
    ! systemctl is-enabled --quiet "$service_name"
}

quiesce_services_for_removal() {
    record_removal_service_state
    if ! quiesce_managed_service "$SERVICE_NAME" "$UNIT_FILE" ||
        ! quiesce_managed_service "$EDGE_SERVICE_NAME" "$EDGE_UNIT_FILE"; then
        restore_removal_service_state
        return 1
    fi
}

uninstall_service() {
    if ! confirm_yes "This removes the systemd service and installed binary but keeps config and data."; then
        log "cancelled"
        return
    fi
    quiesce_services_for_removal || fail "could not stop and disable all managed services; installed files were kept"
    rm -f -- "$UNIT_FILE"
    rm -f -- "$EDGE_UNIT_FILE"
    rm -f -- "$SERVER_BIN"
    rm -f -- "$EDGE_BIN"
    systemctl daemon-reload
    log "uninstalled service and binary; config/data were kept"
}

purge_installation_confirmed() {
    quiesce_services_for_removal || fail "could not stop and disable all managed services; installation paths were kept"
    rm -f -- "$UNIT_FILE"
    rm -f -- "$EDGE_UNIT_FILE"
    systemctl daemon-reload
    safe_remove_path "$INSTALL_DIR"
    safe_remove_path "$CONFIG_DIR"
    safe_remove_path "$DATA_DIR"
    userdel "$SERVICE_USER" 2>/dev/null || true
    groupdel "$SERVICE_GROUP" 2>/dev/null || true
    log "purged VeloraMesh installation paths"
}

purge_installation() {
    if ! confirm_phrase "This removes service, binary, config, TLS files, data, and the service user." "PURGE"; then
        log "cancelled"
        return
    fi
    purge_installation_confirmed
}

guided_fresh_install() {
    local role="${1:-all}"
    role="$(normalize_role "$role")"
    log "guided fresh $(role_label "$role") install"
    if ! confirm_phrase "This deletes all VeloraMesh server data on this host and creates a clean $(role_label "$role") installation." "FRESH"; then
        log "cancelled"
        return
    fi

    purge_installation_confirmed
    guided_install_update "$role"
}

fresh_menu() {
    local choice
    while true; do
        cat <<MENU

VeloraMesh Server - Fresh/Reset By Role
1) Fresh all-in-one API + Edge install/reset
2) Fresh API-only install/reset
3) Fresh Edge-only install/reset
0) Back
MENU
        printf 'Select: '
        read -r choice
        case "$choice" in
            1) guided_fresh_install all; pause ;;
            2) guided_fresh_install api; pause ;;
            3) guided_fresh_install edge; pause ;;
            0) return ;;
            *) log "unknown selection" ;;
        esac
    done
}

reset_store_with_backup() {
    local configured_store_path
    local store_path
    local timestamp
    local backup_dir
    configured_store_path="$(read_env_value VELORAMESH_STORE_PATH)"
    store_path="${configured_store_path:-$DATA_DIR/veloramesh.db}"
    timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
    backup_dir="$DATA_DIR/reset-backup-$timestamp"

    printf 'Store path: %s\n' "$store_path"
    if ! confirm_phrase "This resets networks, members, devices, sessions, and endpoints by moving the store aside." "RESET"; then
        log "cancelled"
        return
    fi

    systemctl stop "$SERVICE_NAME" 2>/dev/null || true
    install -d -m 0750 "$backup_dir"
    for candidate in "$store_path" "$store_path-wal" "$store_path-shm"; do
        if [ -e "$candidate" ]; then
            mv -- "$candidate" "$backup_dir/"
        fi
    done
    log "store moved to backup: $backup_dir"
    if confirm_yes "Start the service now?"; then
        systemctl start "$SERVICE_NAME"
        systemctl --no-pager --full status "$SERVICE_NAME" || true
    fi
}

write_secret_file() {
    local LC_ALL=C
    local first
    local second
    local path="$1"
    while true; do
        first="$(prompt_secret "Join secret (24-256 UTF-8 bytes)")"
        second="$(prompt_secret "Confirm join secret")"
        if [ "$first" != "$second" ]; then
            log "secret confirmation does not match"
        elif [ "${#first}" -lt 24 ] || [ "${#first}" -gt 256 ]; then
            log "join secret must be 24-256 UTF-8 bytes"
        else
            printf '%s' "$first" > "$path"
            chmod 600 "$path"
            return 0
        fi
        if ! confirm_default_yes "Try another join secret?"; then
            return 1
        fi
    done
}

create_network() {
    local name
    local virtual_ipv4_cidr
    local member_limit
    local tmp
    local status=1
    name="$(prompt_required "Network name")" || return
    virtual_ipv4_cidr="$(prompt_virtual_ipv4_cidr "Virtual IPv4 CIDR for this network" "$(configured_or_default VELORAMESH_DEFAULT_VIRTUAL_IPV4_CIDR "$DEFAULT_VIRTUAL_IPV4_CIDR")")"
    member_limit="$(prompt_network_member_limit "$virtual_ipv4_cidr")"
    tmp="$(mktemp)"
    if write_secret_file "$tmp"; then
        admin_safe networks create -name "$name" -join-secret-file "$tmp" -virtual-ipv4-cidr "$virtual_ipv4_cidr" -member-limit "$member_limit"
        status=$?
    fi
    rm -f -- "$tmp"
    return "$status"
}

rotate_network_secret() {
    local name
    local tmp
    local status=1
    name="$(prompt_required "Network name or id")" || return
    tmp="$(mktemp)"
    if write_secret_file "$tmp"; then
        admin_safe networks rotate-secret -network "$name" -join-secret-file "$tmp"
        status=$?
    fi
    rm -f -- "$tmp"
    return "$status"
}

rename_network() {
    local network
    local name
    network="$(prompt_required "Current network name or id")" || return
    name="$(prompt_required "New network name")" || return
    admin_safe networks rename -network "$network" -name "$name"
}

set_network_broadcast_mode() {
    local network
    local mode
    network="$(prompt_required "Network name or id")" || return
    while true; do
        mode="$(prompt_default "LAN discovery broadcast mode (filtered/all)" "filtered")"
        case "$mode" in
            filtered|all)
                admin_safe networks broadcast -network "$network" -mode "$mode"
                return
                ;;
            *)
                log "broadcast mode must be filtered or all"
                ;;
        esac
    done
}

set_network_underlay_ip_mode() {
    local network
    local mode
    network="$(prompt_required "Network name or id")" || return
    while true; do
        mode="$(prompt_default "Underlay IP mode (ipv4-only/dual-stack/ipv6-only)" "ipv4-only")"
        case "$mode" in
            ipv4-only|dual-stack|ipv6-only)
                admin_safe networks underlay -network "$network" -mode "$mode"
                return
                ;;
            *)
                log "underlay IP mode must be ipv4-only, dual-stack, or ipv6-only"
                ;;
        esac
    done
}

set_network_endpoint_ttl() {
    local network
    local seconds
    network="$(prompt_required "Network name or id")" || return
    while true; do
        seconds="$(prompt_default "Endpoint TTL seconds (10-600)" "45")"
        case "$seconds" in
            ''|*[!0-9]*)
                log "endpoint TTL must be a number between 10 and 600"
                ;;
            *)
                if [ "$seconds" -ge 10 ] && [ "$seconds" -le 600 ]; then
                    admin_safe networks endpoint-ttl -network "$network" -seconds "$seconds"
                    return
                fi
                log "endpoint TTL must be a number between 10 and 600"
                ;;
        esac
    done
}

set_network_member_limit() {
    local network
    local limit
    network="$(prompt_required "Network name or id")" || return
    while true; do
        limit="$(prompt_default "Network member limit (1-255; must fit its CIDR)" "255")"
        case "$limit" in
            ''|*[!0-9]*)
                log "network member limit must be a number between 1 and 255"
                ;;
            *)
                if [ "$limit" -ge 1 ] && [ "$limit" -le 255 ]; then
                    admin_safe networks member-limit -network "$network" -limit "$limit"
                    return
                fi
                log "network member limit must be a number between 1 and 255"
                ;;
        esac
    done
}

delete_network() {
    local name
    name="$(prompt_required "Network name or id")" || return
    if confirm_yes "Delete network '$name' and its memberships? Unblocked devices with no other memberships are removed; blocked identities are kept."; then
        admin_safe networks delete -network "$name" -yes
    else
        log "cancelled"
    fi
}

backup_store() {
    local output
    output="$(prompt_required "Backup output path")" || return
    admin_safe store backup -out "$output"
}

restore_store() {
    local input
    input="$(prompt_required "Backup input path")" || return
    if ! confirm_phrase "This overwrites the configured store. Stop the service before restore." "RESTORE"; then
        log "cancelled"
        return
    fi
    systemctl stop "$SERVICE_NAME" 2>/dev/null || true
    admin_safe store restore -in "$input" -yes
    if confirm_yes "Start the service now?"; then
        systemctl start "$SERVICE_NAME"
        systemctl --no-pager --full status "$SERVICE_NAME" || true
    fi
}

show_network() {
    local name
    name="$(prompt_required "Network name or id")" || return
    admin_safe networks show -network "$name"
}

members_for_network() {
    local name
    name="$(prompt_required "Network name or id")" || return
    admin_safe members list -network "$name"
}

member_endpoints() {
    local name
    name="$(prompt_required "Network name or id")" || return
    admin_safe members endpoints -network "$name"
}

remove_member() {
    local network
    local device
    network="$(prompt_required "Network name or id")" || return
    device="$(prompt_required "Device id")" || return
    if confirm_yes "Remove device '$device' from network '$network'?"; then
        admin_safe members remove -network "$network" -device "$device"
    else
        log "cancelled"
    fi
}

show_device() {
    local device
    device="$(prompt_required "Device id")" || return
    admin_safe devices show -device "$device"
}

rename_device() {
    local device
    local name
    device="$(prompt_required "Device id")" || return
    name="$(prompt_required "New device name")" || return
    admin_safe devices rename -device "$device" -name "$name"
}

device_endpoints() {
    local device
    device="$(prompt_required "Device id")" || return
    admin_safe devices endpoints -device "$device"
}

block_device() {
    local device
    device="$(prompt_required "Device id")" || return
    if confirm_yes "Block device '$device' globally?"; then
        admin_safe devices block -device "$device" -yes
    else
        log "cancelled"
    fi
}

unblock_device() {
    local device
    device="$(prompt_required "Device id")" || return
    if confirm_yes "Unblock device '$device'? Old memberships are not silently reactivated."; then
        admin_safe devices unblock -device "$device" -yes
    else
        log "cancelled"
    fi
}

networks_menu() {
    local choice
    while true; do
        cat <<MENU

VeloraMesh Server - Networks
1) List networks
2) Show network
3) Create network
4) Rename network
5) Rotate network join secret
6) Set LAN discovery broadcast mode
7) Set underlay IP mode
8) Set endpoint TTL seconds
9) Set member limit
10) Delete network
0) Back
MENU
        printf 'Select: '
        read -r choice
        case "$choice" in
            1) admin_safe networks list; pause ;;
            2) show_network; pause ;;
            3) create_network; pause ;;
            4) rename_network; pause ;;
            5) rotate_network_secret; pause ;;
            6) set_network_broadcast_mode; pause ;;
            7) set_network_underlay_ip_mode; pause ;;
            8) set_network_endpoint_ttl; pause ;;
            9) set_network_member_limit; pause ;;
            10) delete_network; pause ;;
            0) return ;;
            *) log "unknown selection" ;;
        esac
    done
}

members_menu() {
    local choice
    while true; do
        cat <<MENU

VeloraMesh Server - Members
1) List members in a network
2) List member endpoints in a network
3) Remove member from a network
0) Back
MENU
        printf 'Select: '
        read -r choice
        case "$choice" in
            1) members_for_network; pause ;;
            2) member_endpoints; pause ;;
            3) remove_member; pause ;;
            0) return ;;
            *) log "unknown selection" ;;
        esac
    done
}

devices_menu() {
    local choice
    while true; do
        cat <<MENU

VeloraMesh Server - Devices
1) List devices
2) List blocked devices
3) Show device
4) Rename device
5) Show device endpoints
6) Block device globally
7) Unblock device
0) Back
MENU
        printf 'Select: '
        read -r choice
        case "$choice" in
            1) admin_safe devices list; pause ;;
            2) admin_safe devices blocked; pause ;;
            3) show_device; pause ;;
            4) rename_device; pause ;;
            5) device_endpoints; pause ;;
            6) block_device; pause ;;
            7) unblock_device; pause ;;
            0) return ;;
            *) log "unknown selection" ;;
        esac
    done
}

service_menu() {
    local choice
    local role
    while true; do
        role="$(active_service_role)"
        cat <<MENU

VeloraMesh Server - Services ($(role_label "$role"))
1) Status
2) Logs
3) Restart
4) Stop
5) Start
MENU
        if role_has_api "$role"; then
            cat <<MENU
6) Install/update TLS certificate
7) Backup store
8) Restore store from backup
9) Reset server data with backup
10) Uninstall service, keep config/data
11) Purge installation and data
MENU
        else
            cat <<MENU
6) Uninstall service, keep config/data
7) Purge installation and data
MENU
        fi
        cat <<MENU
0) Back
MENU
        printf 'Select: '
        read -r choice
        case "$choice" in
            1) show_status; pause ;;
            2) show_logs; pause ;;
            3) service_action restart; pause ;;
            4) service_action stop; pause ;;
            5) service_action start; pause ;;
            6)
                if role_has_api "$role"; then
                    update_tls_files_interactive
                else
                    uninstall_service
                fi
                pause
                ;;
            7)
                if role_has_api "$role"; then
                    backup_store
                else
                    purge_installation
                fi
                pause
                ;;
            8)
                if role_has_api "$role"; then
                    restore_store
                    pause
                else
                    log "unknown selection"
                fi
                ;;
            9)
                if role_has_api "$role"; then
                    reset_store_with_backup
                    pause
                else
                    log "unknown selection"
                fi
                ;;
            10)
                if role_has_api "$role"; then
                    uninstall_service
                    pause
                else
                    log "unknown selection"
                fi
                ;;
            11)
                if role_has_api "$role"; then
                    purge_installation
                    pause
                else
                    log "unknown selection"
                fi
                ;;
            0) return ;;
            *) log "unknown selection" ;;
        esac
    done
}

main_menu() {
    local choice
    while true; do
        cat <<MENU

VeloraMesh Server Manager
1) All-in-one API + Edge install/update
2) API-only install/update
3) Edge-only install/update
4) Fresh/reset by role
5) Service operations
6) Network operations
7) Member operations
8) Device operations
9) Admin help
0) Exit
MENU
        printf 'Select: '
        read -r choice
        case "$choice" in
            1) guided_install_update all; pause ;;
            2) guided_install_update api; pause ;;
            3) guided_install_update edge; pause ;;
            4) fresh_menu ;;
            5) service_menu ;;
            6) networks_menu ;;
            7) members_menu ;;
            8) devices_menu ;;
            9) admin_safe help; pause ;;
            0) return ;;
            *) log "unknown selection" ;;
        esac
    done
}

main() {
    local mode="menu"
    local tls_cert_source=""
    local tls_key_source=""
    local approved_spki_pin=""
    while [ "$#" -gt 0 ]; do
        case "$1" in
            --menu) mode="menu"; shift ;;
            --setup) mode="setup"; shift ;;
            --fresh) mode="fresh"; shift ;;
            --install) mode="install"; shift ;;
            --install-tls) mode="install-tls"; shift ;;
            --tls-cert)
                [ "$#" -ge 2 ] || fail "--tls-cert requires a path"
                tls_cert_source="$2"
                shift 2
                ;;
            --tls-key)
                [ "$#" -ge 2 ] || fail "--tls-key requires a path"
                tls_key_source="$2"
                shift 2
                ;;
            --approve-spki-pin)
                [ "$#" -ge 2 ] || fail "--approve-spki-pin requires a sha256/base64 pin"
                approved_spki_pin="$2"
                shift 2
                ;;
            --role)
                [ "$#" -ge 2 ] || fail "--role requires all, api, or edge"
                REQUESTED_ROLE="$(normalize_role "$2")"
                shift 2
                ;;
            --status) mode="status"; shift ;;
            -h|--help) usage; exit 0 ;;
            *) fail "unknown option: $1" ;;
        esac
    done

    require_root
    need_command bash
    need_command systemctl
    need_command journalctl
    need_command mktemp
    need_command install
    need_command chown
    need_command chmod
    need_command find
    need_command getent
    need_command id
    REQUESTED_ROLE="$(normalize_role "$REQUESTED_ROLE")"
    if [ "$mode" != "install-tls" ] && { [ -n "$tls_cert_source" ] || [ -n "$tls_key_source" ] || [ -n "$approved_spki_pin" ]; }; then
        fail "--tls-cert, --tls-key, and --approve-spki-pin are only valid with --install-tls"
    fi

    case "$mode" in
        install) run_install start "$REQUESTED_ROLE" ;;
        install-tls)
            [ -n "$tls_cert_source" ] || fail "--install-tls requires --tls-cert"
            [ -n "$tls_key_source" ] || fail "--install-tls requires --tls-key"
            install_tls_files_non_interactive "$tls_cert_source" "$tls_key_source" "$approved_spki_pin"
            ;;
        setup) guided_install_update "$REQUESTED_ROLE" ;;
        fresh) guided_fresh_install "$REQUESTED_ROLE" ;;
        status) show_status ;;
        menu) main_menu ;;
        *) fail "unknown mode: $mode" ;;
    esac
}

main "$@"
