#!/usr/bin/env bash
#
# hostnamectl - control the system hostname
# Slackware implementation - changes are sent to hostnamed over D-Bus.
# The daemon authorizes requests and owns all system configuration writes.
#
# Copyright 2025, GPLv2+

VALID_CHASSIS=(desktop laptop server tablet handset watch embedded convertible)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

die() {
  echo "hostnamectl: $*" >&2
  exit 1
}

dbus_set() {
  local method="$1"
  shift
  command -v gdbus >/dev/null 2>&1 || die "gdbus is required to change hostname settings"
  gdbus call --system \
    --dest org.freedesktop.hostname1 \
    --object-path /org/freedesktop/hostname1 \
    --method "org.freedesktop.hostname1.${method}" "$@" true >/dev/null \
    || die "D-Bus method ${method} failed"
}

# Read a hostname1 property. gdbus prints a single string variant as
# (< 'value' >,); strip the wrapper for the human-readable status output.
dbus_get() {
  local property="$1"
  local result
  command -v gdbus >/dev/null 2>&1 || die "gdbus is required to read hostname settings"
  result=$(gdbus call --system \
    --dest org.freedesktop.hostname1 \
    --object-path /org/freedesktop/hostname1 \
    --method org.freedesktop.DBus.Properties.Get \
    org.freedesktop.hostname1 "$property" 2>/dev/null) \
    || die "cannot read hostname1 property '$property' from D-Bus"
  # GVariant's CLI printer includes optional spaces around the variant.
  result=$(printf '%s\n' "$result" | sed -e "s/^(< *'//" -e "s/' *>[,]\{0,1\})$//")
  printf '%s' "$result"
}

validate_meta_value() {
  local value="$1" key="$2"
  if printf '%s' "$value" | grep -qP '[\x00-\x1f\x7f"\\]'; then
    die "invalid characters in value for $key"
  fi
}

dmi_read() {
  cat "${DMI_DIR:-/sys/class/dmi/id}/$1" 2>/dev/null || true
}

detect_virtualization() {
  if [ -f /.dockerenv ] || grep -q "docker\|container" /proc/1/cgroup 2>/dev/null; then
    echo "docker"; return
  fi
  if grep -qi "hypervisor" /proc/cpuinfo 2>/dev/null; then
    local vendor
    vendor=$(dmi_read sys_vendor)
    case "$vendor" in
      *VMware*)     echo "vmware"    ;;
      *VirtualBox*) echo "oracle"    ;;
      *QEMU*)       echo "kvm"       ;;
      *Microsoft*)  echo "microsoft" ;;
      *Xen*)        echo "xen"       ;;
      *)            echo "vm-other"  ;;
    esac
    return
  fi
  echo "none"
}

# ---------------------------------------------------------------------------
# status
# ---------------------------------------------------------------------------

cmd_status() {
  local static_hostname transient_hostname pretty_hostname
  local icon_name chassis deployment location
  local machine_id boot_id virtualization
  local os_name os_cpe kernel arch
  local hw_vendor hw_model fw_version fw_date

  # hostname1 properties are the authoritative view exposed to clients.
  static_hostname=$(dbus_get StaticHostname)
  [ -z "$static_hostname" ] && static_hostname="n/a"

  transient_hostname=$(dbus_get Hostname)
  [ -z "$transient_hostname" ] && transient_hostname="n/a"

  pretty_hostname=$(dbus_get PrettyHostname)
  icon_name=$(dbus_get IconName)
  [ -z "$icon_name" ] && icon_name="computer"
  chassis=$(dbus_get Chassis)
  deployment=$(dbus_get Deployment)
  location=$(dbus_get Location)

  machine_id=$(cat /etc/machine-id 2>/dev/null | tr -d '[:space:]')
  [ -z "$machine_id" ] && machine_id="n/a"

  boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null | tr -d '[:space:]')
  [ -z "$boot_id" ] && boot_id="n/a"

  virtualization=$(detect_virtualization)

  os_name=$(dbus_get OperatingSystemPrettyName)
  [ -z "$os_name" ] && os_name="Unknown OS"
  os_cpe=$(dbus_get OperatingSystemCPEName)

  kernel=$(dbus_get KernelRelease)
  [ -z "$kernel" ] && kernel=$(uname -r)
  arch=$(uname -m)

  dt_dir=${DT_DIR:-/sys/firmware/devicetree/base}
  dt_model=""
  dt_vendor=""
  # Only touch the device tree when the properties exist (they don't on x86);
  # test first so a missing file never produces a shell redirection error.
  if [ -r "$dt_dir/model" ]; then
    dt_model=$(tr -d '\0' < "$dt_dir/model")
  fi
  if [ -r "$dt_dir/compatible" ]; then
    dt_vendor=$(tr '\0' '\n' < "$dt_dir/compatible" | head -n1 | cut -d, -f1)
  fi
  hw_vendor=$(dbus_get HardwareVendor)
  hw_model=$(dbus_get HardwareModel)
  fw_version=$(dbus_get FirmwareVersion)
  fw_date=$(dmi_read bios_date)

  printf "   Static hostname: %s\n" "$static_hostname"
  [ "$transient_hostname" != "$static_hostname" ] && \
    printf "Transient hostname: %s\n" "$transient_hostname"
  [ -n "$pretty_hostname" ] && \
    printf "   Pretty hostname: %s\n" "$pretty_hostname"
  printf "         Icon name: %s\n" "$icon_name"
  printf "           Chassis: %s\n" "$chassis"
  [ -n "$deployment" ] && \
    printf "        Deployment: %s\n" "$deployment"
  [ -n "$location" ] && \
    printf "          Location: %s\n" "$location"
  printf "        Machine ID: %s\n" "$machine_id"
  printf "           Boot ID: %s\n" "$boot_id"
  [ "$virtualization" != "none" ] && \
    printf "    Virtualization: %s\n" "$virtualization"
  printf "  Operating System: %s\n" "$os_name"
  [ -n "$os_cpe" ] && \
    printf "            CPE OS: %s\n" "$os_cpe"
  [ -n "$hw_vendor" ] && \
    printf "   Hardware Vendor: %s\n" "$hw_vendor"
  [ -n "$hw_model" ] && \
    printf "    Hardware Model: %s\n" "$hw_model"
  [ -n "$fw_version" ] && \
    printf "  Firmware Version: %s\n" "$fw_version"
  [ -n "$fw_date" ] && \
    printf "     Firmware Date: %s\n" "$fw_date"
  printf "            Kernel: Linux %s\n" "$kernel"
  printf "      Architecture: %s\n" "$arch"
}

# ---------------------------------------------------------------------------
# set-hostname [--static|--transient|--pretty] NAME
# ---------------------------------------------------------------------------

cmd_set_hostname() {
  local mode="all"
  local name=""

  while [ $# -gt 0 ]; do
    case "$1" in
      --static)    mode="static"    ;;
      --transient) mode="transient" ;;
      --pretty)    mode="pretty"    ;;
      -*)          die "unknown option '$1'" ;;
      *)           name="$1" ;;
    esac
    shift
  done

  [ -z "$name" ] && die "no hostname given"

  if [ "$mode" != "pretty" ]; then
    echo "$name" | grep -qE '^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?))*$' \
      || die "invalid hostname '$name'"
  else
    validate_meta_value "$name" "PRETTY_HOSTNAME"
  fi

  case "$mode" in
    static)    dbus_set SetStaticHostname "$name" ;;
    transient) dbus_set SetHostname "$name" ;;
    pretty)    dbus_set SetPrettyHostname "$name" ;;
    all)
      # SetStaticHostname persists the name and updates the running hostname.
      dbus_set SetStaticHostname "$name"
      ;;
  esac

  echo "Hostname set to '$name'."
}

# ---------------------------------------------------------------------------
# set-icon-name NAME
# ---------------------------------------------------------------------------

cmd_set_icon_name() {
  [ -z "${1:-}" ] && die "no icon name given"
  validate_meta_value "$1" "ICON_NAME"
  dbus_set SetIconName "$1"
  echo "Icon name set to '$1'."
}

# ---------------------------------------------------------------------------
# set-chassis TYPE
# ---------------------------------------------------------------------------

cmd_set_chassis() {
  [ -z "${1:-}" ] && die "no chassis type given"

  local valid=0
  for c in "${VALID_CHASSIS[@]}"; do
    [ "$c" = "$1" ] && valid=1 && break
  done
  [ $valid -eq 0 ] && die "invalid chassis '$1'. Valid: ${VALID_CHASSIS[*]}"

  dbus_set SetChassis "$1"
  echo "Chassis set to '$1'."
}

# ---------------------------------------------------------------------------
# set-deployment ENV
# ---------------------------------------------------------------------------

cmd_set_deployment() {
  [ -z "${1:-}" ] && die "no deployment environment given"
  validate_meta_value "$1" "DEPLOYMENT"
  dbus_set SetDeployment "$1"
  echo "Deployment set to '$1'."
}

# ---------------------------------------------------------------------------
# set-location LOCATION
# ---------------------------------------------------------------------------

cmd_set_location() {
  [ -z "${1:-}" ] && die "no location given"
  validate_meta_value "$1" "LOCATION"
  dbus_set SetLocation "$1"
  echo "Location set to '$1'."
}

# ---------------------------------------------------------------------------
# usage
# ---------------------------------------------------------------------------

usage() {
  cat <<EOF
Usage: hostnamectl [OPTIONS] COMMAND

Commands:
  status                                   Show current hostname and system info
  set-hostname [--static|--transient|--pretty] NAME
                                           Set system hostname
                                                                                   (default: sets static and transient)
  set-icon-name NAME                       Set icon name
  set-chassis TYPE                         Set chassis type
                                           (${VALID_CHASSIS[*]})
  set-deployment ENV                       Set deployment environment
  set-location LOCATION                    Set location string

Options:
  -h, --help                               Show this help

Note: Changes are sent to hostnamed over the system D-Bus. Polkit controls
      authorization; hostnamed writes the configuration files.
EOF
}

# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

COMMAND="${1:-status}"
shift 2>/dev/null || true

case "$COMMAND" in
  status)          cmd_status         "$@" ;;
  set-hostname)    cmd_set_hostname   "$@" ;;
  set-icon-name)   cmd_set_icon_name  "$@" ;;
  set-chassis)     cmd_set_chassis    "$@" ;;
  set-deployment)  cmd_set_deployment "$@" ;;
  set-location)    cmd_set_location   "$@" ;;
  -h|--help|help)  usage ;;
  *) die "unknown command '$COMMAND'. Try --help." ;;
esac
