#!/usr/bin/env bash
#
# hostnamectl - control the system hostname
# Slackware implementation - writes files directly.
# hostnamed picks up changes via inotify and updates D-Bus properties.
#
# Copyright 2025, GPLv2+

if [ -f /etc/HOSTNAME ]; then
  HOSTNAME_FILE="/etc/HOSTNAME"
else
  HOSTNAME_FILE="/etc/hostname"
fi

MACHINE_INFO_FILE="/etc/machine-info"

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

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

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

require_root() {
  [ "$EUID" -ne 0 ] && die "must be root to change hostname settings"
  return 0
}

# Reject control characters (including newline/tab/CR), double-quote, and
# backslash — mirrors rcl_machine_info_value_is_safe() in the daemon.
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
}

get_os_release() {
  local key="$1"
  [ -f /etc/os-release ] && grep "^${key}=" /etc/os-release | cut -d= -f2- | tr -d '"'
}

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

machine_info_get() {
  grep "^${1}=" "$MACHINE_INFO_FILE" 2>/dev/null | cut -d= -f2- | tr -d '"' || true
}

# Atomically write or remove a KEY="value" entry in /etc/machine-info.
# Passing an empty value removes the key entirely.
machine_info_set() {
  local key="$1"
  local value="$2"
  local tmp

  touch "$MACHINE_INFO_FILE" 2>/dev/null || die "cannot write $MACHINE_INFO_FILE"

  tmp=$(mktemp "${MACHINE_INFO_FILE}.XXXXXX") || die "cannot create temp file"

  grep -v "^${key}=" "$MACHINE_INFO_FILE" > "$tmp" 2>/dev/null || true

  if [ -n "$value" ]; then
    echo "${key}=\"${value}\"" >> "$tmp"
  fi

  mv "$tmp" "$MACHINE_INFO_FILE" || { rm -f "$tmp"; die "cannot update $MACHINE_INFO_FILE"; }
}

# Write out a fully populated /etc/machine-info from detected values.
# Called automatically if the file does not exist yet.
# An existing file is never overwritten by this function.
init_machine_info() {
  [ -f "$MACHINE_INFO_FILE" ] && return

  local chassis icon pretty
  local ct
  ct=$(dmi_read chassis_type)
  case "$ct" in
    3|4|6|7)    chassis="desktop";     icon="computer-desktop" ;;
    8|9|10|14)  chassis="laptop";      icon="computer-laptop"  ;;
    11)         chassis="handset";     icon="phone"            ;;
    17|23)      chassis="server";      icon="computer-server"  ;;
    30|32)      chassis="tablet";      icon="computer-tablet"  ;;
    31)         chassis="convertible"; icon="computer-laptop"  ;;
    *)          chassis="";            icon="computer"         ;;
  esac

  # Use the static hostname as the initial pretty hostname
  pretty=$(cat "$HOSTNAME_FILE" 2>/dev/null | tr -d '[:space:]' | cut -d. -f1)

  local tmp
  tmp=$(mktemp "${MACHINE_INFO_FILE}.XXXXXX") || return

  cat > "$tmp" << 'EOF'
# /etc/machine-info - see machine-info(5)
EOF
  printf 'PRETTY_HOSTNAME="%s"\n' "$pretty" >> "$tmp"
  printf 'ICON_NAME="%s"\n'       "$icon"   >> "$tmp"
  printf 'CHASSIS="%s"\n'         "$chassis" >> "$tmp"
  printf 'DEPLOYMENT=""\n'                   >> "$tmp"
  printf 'LOCATION=""\n'                     >> "$tmp"

  mv "$tmp" "$MACHINE_INFO_FILE" || rm -f "$tmp"
  echo "Created $MACHINE_INFO_FILE"
}

detect_chassis() {
  local chassis
  chassis=$(machine_info_get CHASSIS)
  [ -n "$chassis" ] && { echo "$chassis"; return; }

  local ct
  ct=$(dmi_read chassis_type)
  case "$ct" in
    3|4|6|7)    echo "desktop"     ;;
    8|9|10|14)  echo "laptop"      ;;
    11)         echo "handset"     ;;
    17|23)      echo "server"      ;;
    30|32)      echo "tablet"      ;;
    31)         echo "convertible" ;;
    *)          echo "unknown"     ;;
  esac
}

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

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

  transient_hostname=$(hostname 2>/dev/null || true)
  [ -z "$transient_hostname" ] && transient_hostname="n/a"

  pretty_hostname=$(machine_info_get PRETTY_HOSTNAME)
  icon_name=$(machine_info_get ICON_NAME)
  [ -z "$icon_name" ] && icon_name="computer"
  chassis=$(detect_chassis)
  deployment=$(machine_info_get DEPLOYMENT)
  location=$(machine_info_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=$(get_os_release PRETTY_NAME)
  [ -z "$os_name" ] && os_name=$(get_os_release NAME)
  [ -z "$os_name" ] && os_name="Unknown OS"
  os_cpe=$(get_os_release CPE_NAME)

  kernel=$(uname -r)
  arch=$(uname -m)

  hw_vendor=$(dmi_read sys_vendor)
  hw_model=$(dmi_read product_name)
  fw_version=$(dmi_read bios_version)
  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() {
  require_root

  local mode
  local name
  mode="all"
  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'"
  fi

  case "$mode" in
    static)
      echo "$name" > "$HOSTNAME_FILE" || die "cannot write $HOSTNAME_FILE"
      ;;
    transient)
      hostname "$name" || die "cannot set transient hostname"
      ;;
    pretty)
      validate_meta_value "$name" "PRETTY_HOSTNAME"
      machine_info_set "PRETTY_HOSTNAME" "$name"
      ;;
    all)
      echo "$name" > "$HOSTNAME_FILE" || die "cannot write $HOSTNAME_FILE"
      hostname "$name" || die "cannot set transient hostname"
      # Update pretty hostname to the short name (strip domain)
      local short
      short=$(echo "$name" | cut -d. -f1)
      machine_info_set "PRETTY_HOSTNAME" "$short"
      ;;
  esac

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

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

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

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

cmd_set_chassis() {
  require_root
  [ -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[*]}"

  machine_info_set "CHASSIS" "$1"
  echo "Chassis set to '$1'."
}

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

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

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

cmd_set_location() {
  require_root
  [ -z "${1:-}" ] && die "no location given"
  validate_meta_value "$1" "LOCATION"
  machine_info_set "LOCATION" "$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 both 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 written directly to /etc/HOSTNAME and /etc/machine-info.
      hostnamed picks them up via inotify and updates D-Bus properties
      automatically.
EOF
}

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

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

# Bootstrap /etc/machine-info on first run
if [ ! -f "$MACHINE_INFO_FILE" ] && [ "$EUID" -eq 0 ]; then
  init_machine_info
fi

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
