#!/usr/bin/env bash
# ==========================================================================
#         ____            _                     _____           _
#        / ___| _   _ ___| |_ ___ _ __ ___     |_   _|__   ___ | |___
#        \___ \| | | / __| __/ _ \ '_ ` _ \ _____| |/ _ \ / _ \| / __|
#         ___) | |_| \__ \ ||  __/ | | | | |_____| | (_) | (_) | \__ \
#        |____/ \__, |___/\__\___|_| |_| |_|     |_|\___/ \___/|_|___/
#               |___/
#                             --- System-Tools ---
#                  https://www.nntb.no/~dreibh/system-tools/
# ==========================================================================
#
# System-Info
# Copyright (C) 2013-2026 by Thomas Dreibholz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Contact: thomas.dreibholz@gmail.com

# Bash options:
set -euo pipefail

# gettext options:
export TEXTDOMAIN="System-Info"
# export TEXTDOMAINDIR="${PWD}/locale"   # Default: "/usr/share/locale"

# shellcheck disable=SC1091
. gettext.sh


# ###### Settings ###########################################################
is_ltr=1
rlm=""
lrm=""
if [ "$(gettext "LTR")" == "RTL" ] ; then
   is_ltr=0
   rlm=$'\u200F'   # Right-to-Left Marker (RLM)
   lrm=$'\u200E'   # Left-to-Right Marker (LRM)
fi

colorBasic="\e[33m"
colorNetwork="\e[34m"
colorPrefix="\e[36m"
colorInactive="\e[37m"
colorNone="\e[0m"

colorBatteryFull="\e[32m"
colorBatteryCharging="\e[33m"
colorBatteryNotCharging="\e[38m"
colorBatteryDischarging="\e[35m"

colorDiskNormal="\e[32m"
colorDisk80="\e[33m"
colorDisk95="\e[31m"


# ###### Print banners ######################################################
function print-banners ()
{
   local directory
   for directory in "$@" ; do
      if [ -d "${directory}" ] ; then
         local bannerScript
         find "${directory}"  -maxdepth 1 -type f -name "[0-9][0-9]*[^~]" -print0 | sort -rz | while IFS= read -r -d "" bannerScript ; do
            "${bannerScript}" || break
         done
      fi
   done
}


# ###### Begin the system information table #################################
tableColumnLabels=
tableColumnValues=
function begin-table()
{
   tableColumnLabels=( )
   tableColumnValues=( )
}


# ###### Finish and print the system information table ######################
function finish-table()
{
   local i
   local label
   local value
   local indent=0

   # ====== Compute indentation =============================================
   for ((i=0; i < ${#tableColumnLabels[@]}; i++)) ; do
      label="${tableColumnLabels[$i]}"
      width="$(print-utf8 --width "${label}")"
      if [ "${width}" -gt "${indent}" ] ; then
         indent="${width}"
      fi
   done
   indent=-$((indent + 1))

   # ====== Print table rows ================================================
   for ((i=0; i < ${#tableColumnLabels[@]}; i++)) ; do
      label="${tableColumnLabels[$i]}"
      value="${tableColumnValues[$i]}"

      # ====== Left-to-Right (LTR) locale =================================
      if [ ${is_ltr} -eq 1 ] ; then
         print-utf8 -i "${indent}" "${label}"
         echo -e "${value}"
      # ====== Right-to-Left (RTL) locale =================================
      else
         # Insert Right-to-Left Mark (RLM) to force treating the line as RTL
         echo -ne "${rlm}"
         print-utf8 -i "${indent}" "${label}"
         echo -ne "${rlm}"
         echo -e "${value}"
      fi

   done
}


# ###### Add a row to the table #############################################
function add-table-row ()
{
   local label="$1"
   local value="$2"
   tableColumnLabels+=( "${label}" )
   tableColumnValues+=( "${value}" )
}


# ###### Print system information ###########################################
function print-system-information ()
{
   local testSystemInfoInput="$1"

   # ====== Obtain system information =======================================
   if ! command -v get-system-info >/dev/null ; then
      gettext >&2 \
         "ERROR: get-system-info not found in search path!"
      echo >&2
      exit 1
   fi

   # Initialise variables with defaults:
   hostname_long=""
   uptime_days=0
   uptime_hours=0
   uptime_mins=0
   uptime_secs=0
   system_sysname=""
   system_release=""
   system_users=0
   system_cores=0
   system_machine=""
   system_procs=0
   system_load_avg1min=0
   system_load_avg5min=0
   system_load_avg15min=0
   battery_list=""
   system_mem_used=0
   system_mem_total=0
   system_mem_free=0
   system_mem_freepct=0
   system_swap_total=0
   system_swap_used=0
   system_swap_free=0
   system_swap_freepct=0
   disk_list=""
   netif_list=""

   # Set the variables:
   if [ "${testSystemInfoInput}" == "" ] ; then
      eval "$(get-system-info)"
   else
      eval "$(cat "${testSystemInfoInput}")"
   fi

   # ====== Obtain operating system information =============================
   DISTRIB_ID="$(gettext "Unknown")"   # Unknown, yet.
   DISTRIB_RELEASE="${DISTRIB_ID}"     # Unknown, yet.
   DISTRIB_CODENAME=""

   # ------ Get information from /etc/os-release ----------------------------
   if [ -e /etc/os-release ] ; then
      # shellcheck disable=SC1091
      . /etc/os-release
      if [ -v NAME ] ; then
         DISTRIB_ID="${NAME}"
      fi
      if [ -v VERSION_ID ] ; then
         DISTRIB_RELEASE="${VERSION_ID}"
      fi
      if [ -v VERSION ] && [[ "${VERSION}" =~ ^.*\(([^\(\)]*)\)$ ]] ; then
         DISTRIB_CODENAME="${BASH_REMATCH[1]}"
      elif [ -v VERSION_CODENAME ] ; then
         DISTRIB_CODENAME="${VERSION_CODENAME}"
      fi

   # ------ Legacy system: try lsb_release ----------------------------------
   elif command -v lsb_release >/dev/null ; then
      DISTRIB_ID="$(lsb_release -is)"
      DISTRIB_RELEASE="$(lsb_release -rs)"
      DISTRIB_CODENAME="$(lsb_release -cs)"

   # ------ Otherwise, use what is provided from the kernel -----------------
   else
      DISTRIB_ID="${system_sysname}"
      DISTRIB_RELEASE="${system_release}"
      DISTRIB_CODENAME=""
   fi

   local codeNameText=""
   if [ "${DISTRIB_CODENAME}" != "" ] ; then
      codeNameText=" (${DISTRIB_CODENAME})"
   fi
   # shellcheck disable=SC2034
   local os_label="${DISTRIB_ID} ${DISTRIB_RELEASE}${codeNameText}"


   # ====== Print system information ========================================
   echo -en "${colorBasic}"
   begin-table

   # ------ Hostname -------------------------------------------------------
   add-table-row \
      "$(gettext "Host:")" \
      "${hostname_long}"

   # ------ Uptime ----------------------------------------------------------
   # xgettext: c-format
   uptimeFormat="$(gettext "%s, %s, %s, and %s")"
   # xgettext: c-format
   formatUptimeDays="$(ngettext "%s day" "%s days" "${uptime_days}")"
   # xgettext: c-format
   formatUptimeHours="$(ngettext "%d hour" "%d hours" "${uptime_hours}")"
   # xgettext: c-format
   formatUptimeMinutes="$(ngettext "%d minute" "%d minutes" "${uptime_mins}")"
   # xgettext: c-format
   formatUptimeSeconds="$(ngettext "%d second" "%d seconds" "${uptime_secs}")"

   # shellcheck disable=SC2059
   add-table-row \
      "$(gettext "Uptime:")" \
      "$(printf "${uptimeFormat}\n" \
         "$(printf "${formatUptimeDays}"    "$(printf "%'d" "${uptime_days}")")" \
         "$(printf "${formatUptimeHours}"   "${uptime_hours}")"                  \
         "$(printf "${formatUptimeMinutes}" "${uptime_mins}")"                   \
         "$(printf "${formatUptimeSeconds}" "${uptime_secs}")"
      )"

   # ------ Operating System ------------------------------------------------
   add-table-row \
      "$(gettext "Operating System:")" \
      "$(eval_gettext "\${os_label} with kernel \${system_release}")"

   # ------ Processor -------------------------------------------------------
   # xgettext: c-format
   processesFormat="$(gettext "%s processes")"
   # shellcheck disable=SC2034,SC2059
   processesString="$(printf "${processesFormat}" "${system_procs}")"
   # xgettext: c-format
   usersFormat="$(ngettext "%s user" "%s users" "${system_users}")"
   # shellcheck disable=SC2034,SC2059
   usersString="$(printf "${usersFormat}" "$(printf "%'d" "${system_users}")")"
   # shellcheck disable=SC2034
   systemCoresString="$(printf "%'d" "${system_cores}")"
   # shellcheck disable=SC2034
   systemMachineString="${system_machine}"

   # shellcheck disable=SC2034,SC2059
   add-table-row \
      "$(gettext "Processor:")" \
      "$(eval_gettext \
         "\${systemCoresString} × \${systemMachineString}; \${processesString}; \${usersString}"
       )"

   # ------ Load ------------------------------------------------------------
   # xgettext: c-format
   loadFormat="$(gettext "%1.1f %% / %1.1f %% / %1.1f %%")"

   # shellcheck disable=SC2059
   add-table-row \
      "$(gettext "Load (1/5/15 min):")" \
      "$(printf "${loadFormat}\n" \
         "${system_load_avg1min}" \
         "${system_load_avg5min}" \
         "${system_load_avg15min}"
       )"

   # ------ Battery ---------------------------------------------------------
   if [ "${battery_list}" != "" ] ; then
      local batterySummary=( )
      local battery
      local batteryNumber=0
      local batteryStatusVariable
      local batteryCapacityVariable
      local batteryStatus
      local batteryCapacity
      local colorBattery
      local separator=""
      for battery in ${battery_list} ; do
         batteryNumber=$((batteryNumber+1))
         if [ ${batteryNumber} -gt 1 ] ; then
            separator=" |  "
         fi
         batteryStatusVariable="battery_${battery}_status"
         batteryCapacityVariable="battery_${battery}_capacity"
         batteryStatus="$(gettext "unknown")"
         colorBattery="${colorInactive}"
         if [[ "${!batteryStatusVariable}" =~ ^[0-9]+$ ]] ; then
            if [ "${!batteryStatusVariable}" -eq 1 ] ; then
               batteryStatus="$(gettext "not charging")"
               colorBattery="${colorBatteryNotCharging}"
            elif [ "${!batteryStatusVariable}" -eq 2 ] ; then
               batteryStatus="$(gettext "charging")"
               colorBattery="${colorBatteryCharging}"
            elif [ "${!batteryStatusVariable}" -eq 3 ] ; then
               batteryStatus="$(gettext "full")"
               colorBattery="${colorBatteryFull}"
            elif [ "${!batteryStatusVariable}" -eq 4 ] ; then
               # shellcheck disable=SC2034
               batteryStatus="$(gettext "discharging")"
               colorBattery="${colorBatteryDischarging}"
            fi
         fi
         # shellcheck disable=SC2034
         batteryCapacity="${!batteryCapacityVariable}"

         batterySummary+=( "${separator}${colorBattery}$(eval_gettext \
            "#\${batteryNumber} \${batteryStatus}, \${batteryCapacity} %")${colorBasic}" )
      done

      add-table-row \
         "$(gettext "Battery:")" \
         "${batterySummary[*]}"
   fi

   # ------ Memory ---------------------------------------------------------
   # NOTE: Using strings (%s) for formatting, since this is easier to handle
   #       by translation AI. Patterns like %'5.1f often fail to translate.

   # xgettext: c-format
   memoryFormat=$(gettext "%s MiB of %s MiB (%s MiB available: %s %%)")

   # shellcheck disable=SC2059
   add-table-row \
      "$(gettext "Used Memory:")" \
      "$(printf "${memoryFormat}\n" \
         "$(printf "%'6.0f" "$((system_mem_used  / 1048576))")" \
         "$(printf "%'6.0f" "$((system_mem_total / 1048576))")" \
         "$(printf "%'6.0f" "$((system_mem_free  / 1048576))")" \
         "$(printf "%'5.1f" "${system_mem_freepct}")"
       )"
   if [ "${system_swap_total}" != "" ] && [ "${system_swap_total}" -gt 0 ] ; then
      # shellcheck disable=SC2059
      add-table-row \
         "$(gettext "Used Swap:")" \
         "$(printf "${memoryFormat}\n" \
            "$(printf "%'6.0f" "$((system_swap_used  / 1048576))")" \
            "$(printf "%'6.0f" "$((system_swap_total / 1048576))")" \
            "$(printf "%'6.0f" "$((system_swap_free  / 1048576))")" \
            "$(printf "%'5.1f" "${system_swap_freepct}")"
          )"
   else
      add-table-row \
         "$(gettext "Used Swap:")" \
         "${colorInactive}$(gettext "no swap available!")${colorBasic}"
   fi

   # ------ Storage ---------------------------------------------------------
   local diskSummary=( )
   local mountPointVariable
   local mountPointName
   local mountPoint
   local percentage
   local colorDisk
   local separator
   for mountPointName in ${disk_list} ; do
      if [ "${mountPointName}" == "root" ] ; then
         mountPoint="/"
         separator=""
      else
         mountPoint="/${mountPointName}"
         separator=" |  "
      fi
      mountPointVariable="disk_${mountPointName}_pct"
      if [ -v "${mountPointVariable}" ] && [[ "${!mountPointVariable}" =~ ^([0-9]+) ]]; then
         percentage="${BASH_REMATCH[1]}"   # The numerical value before the decimal point
         if [ "${percentage}" -ge 95 ] ; then
            colorDisk="${colorDisk95}"
         elif [ "${percentage}" -ge 80 ] ; then
            colorDisk="${colorDisk80}"
         else
            colorDisk="${colorDiskNormal}"
         fi

         # xgettext: c-format
         # shellcheck disable=SC2059
         diskSummary+=( "${separator}${colorDisk}$(printf "$(gettext "%1.0f %% on %s")" \
            "${!mountPointVariable}" \
            "${lrm}${mountPoint}${rlm}")${colorBasic}" )
      fi
   done
   add-table-row \
      "$(gettext "Used Diskspace:")" \
      "${diskSummary[*]}"

   # ------ SSH Key Fingerprints --------------------------------------------
   print-ssh-key-fingerprints

   # ------ Network ---------------------------------------------------------
   add-table-row \
      "$(gettext "Network:")" ""

   for interface in ${netif_list} ; do
      local nameVariable="netif_${interface}_name"
      local flagsVariable="netif_${interface}_flags"
      local ipv4Variable="netif_${interface}_ipv4"
      local ipv6Variable="netif_${interface}_ipv6"

      if [ -v "${ipv4Variable}" ] || [ -v "${ipv6Variable}" ] ; then

         if [[ "${!flagsVariable}" =~ (<UP>) ]] ; then
            # Interface is UP
            colorA="${colorNetwork}"
            colorP="${colorPrefix}"
         else
            # Interface is DOWN
            colorA="${colorInactive}"
            colorP="${colorInactive}"
         fi
         local ipv4
         if [ -v "${ipv4Variable}" ] ; then
            ipv4="${!ipv4Variable}"
            ipv4="${ipv4// /${colorA} }"
            ipv4="${ipv4//\//${colorP}\/}"
            ipv4="${ipv4// /   }"
            ipv4="${ipv4//\// \/ }"
         else
            ipv4="$(gettext "(No IPv4)")"
         fi
         add-table-row "   ${rlm}${colorA}${!nameVariable}:" "${lrm}${ipv4}${colorA}"

         if [ -v "${ipv6Variable}" ] ; then
            local v6addrList=( "${!ipv6Variable}" )
            local ipv6
            # shellcheck disable=SC2068
            for ipv6 in ${v6addrList[@]} ; do
               ipv6="${ipv6// /${colorA} }"
               ipv6="${ipv6//\//${colorP}\/}"
               ipv6="${ipv6//\// \/ }"
               ipv6="${ipv6//%/%%}"
               add-table-row "" "${ipv6}${colorA}"
            done
         fi

      fi
   done

   # ------ Print the system information table ------------------------------
   echo -en "${colorBasic}"
   finish-table
   echo -en "${colorNone}"
}


# ###### Print SSH public key fingerprints ##################################
function print-ssh-key-fingerprints ()
{
   if [ -d /etc/ssh ] && command -v ssh-keygen >/dev/null ; then
      local keyNumber=1
      local keyInfo
      local key
      local label
      while IFS= read -r -d "" key ; do
         if [[ ! "${key}" =~ _dsa_key.pub$ ]] ; then
            keyInfo="$(ssh-keygen -lf "$key" | sed -e "s/^\([0-9]*\) \([^ ]*\) \(.*\) (\(.*\))$/\2 (\4 \1)/g")"
            if [[ "${keyInfo}" =~ ^[A-Z]+ ]] ; then
               if [ ${keyNumber} -eq 1 ] ; then
                  label="$(gettext "SSH Keys:")"
               else
                  label=""
               fi
               add-table-row \
                  "${label}" \
                  "$(eval_gettext \
                     "#\${keyNumber} \${keyInfo}"
                  )"
               keyNumber=$((keyNumber+1))
            fi
         fi
      done < <(find /etc/ssh -maxdepth 1 -name "ssh_host_*.pub" -print0 | sort -z)
   fi
}


# ###### Usage ##############################################################
usage () {
   local exitCode="$1"
   echo >&2 "$(gettext "Usage:") $0 [-S|--scripts scripts_directory] [-X|--test-date date_string] [-Y|--test-system-info input_file [-h|--help] [-v|--version]"
   exit "${exitCode}"
}


# ###### Version ############################################################
version () {
   echo "System-Info @BUILD_MAJOR@.@BUILD_MINOR@.@BUILD_PATCH@"
   exit 0
}



# ###### Main program #######################################################

# ====== Handle arguments ===================================================
if (( BASH_VERSINFO[0] < 4 )) || (( (BASH_VERSINFO[0] == 4) && (BASH_VERSINFO[1] < 4) )) ; then
   echo "ERROR: Bash 4.4 or higher is required, but Bash ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]} is installed!"
   exit 1
fi
GETOPT="$(PATH="/usr/local/bin:${PATH}" command -v getopt)"
if "${GETOPT}" -T >/dev/null 2>&1 || [[ $? -ne 4 ]]; then
   echo >&2 "ERROR: Enhanced/GNU getopt is required!"
   exit 1
fi
options="$(${GETOPT} -o S:X:Y:hv --long scripts:,test-date:,test-system-info:,help,version -a -- "$@")"
# shellcheck disable=SC2181
if [[ $? -ne 0 ]]; then
   usage 1
fi
eval set -- "${options}"

# shellcheck disable=SC2034
now="$(date)"
testSystemInfoInput=""
scriptDirectories=( "/etc/system-info.d" "/usr/local/etc/system-info.d" )
while [ $# -gt 0 ] ; do
   case "$1" in
      -S | --scripts)
         scriptDirectories=( "$2" )
         shift
         ;;
      -X | --test-date)
         # shellcheck disable=SC2034
         now="$2"
         shift
         ;;
      -Y | --test-system-info)
         testSystemInfoInput="$2"
         if [ ! -e "${testSystemInfoInput}" ] ; then
            eval_gettext >&2 "ERROR: Unable to find input file \${testSystemInfoInput}!"
            echo >&2
            exit 1
         fi
         shift
         ;;
      -h | --help)
         usage 0
         ;;
      -v | --version)
         version
         ;;
      --)
         shift
         break
         ;;
      *)
         # This should not happen: wrong getopt parameters, or missing case?
         echo >&2 "INTERNAL ERROR: Unhandled option $1!"
         exit 1
         ;;
  esac
  shift
done
if [ $# -ne 0 ] ; then
   usage 1
fi


# ====== Startup ============================================================
echo
eval_gettext "System information on \${now}"
echo ; echo

# ====== Obtain and print information =======================================
# Getting the network information may take a few hundreds of ms on a router
# => prepare it in background, while printing other information.
# shellcheck disable=SC2086
(
   print-system-information "${testSystemInfoInput}"
) 2>&1 | (
   print-banners "${scriptDirectories[@]}"
   cat
) | \
(
   # Try output via mbuffer. This is faster for console printing, particularly
   # with complex banners and long interface lists. If unavailable, try buffer.
   # Just use cat as fallback, if neither mbuffer nor buffer are available.
   if ! mbuffer -q -s 16k 2>/dev/null ; then
      if ! buffer -s 16k 2>/dev/null ; then
         cat
      fi
   fi
)
