#!/usr/bin/env bash
# ==========================================================================
#         ____            _                     _____           _
#        / ___| _   _ ___| |_ ___ _ __ ___     |_   _|__   ___ | |___
#        \___ \| | | / __| __/ _ \ '_ ` _ \ _____| |/ _ \ / _ \| / __|
#         ___) | |_| \__ \ ||  __/ | | | | |_____| | (_) | (_) | \__ \
#        |____/ \__, |___/\__\___|_| |_| |_|     |_|\___/ \___/|_|___/
#               |___/
#                             --- System-Tools ---
#                  https://www.nntb.no/~dreibh/system-tools/
# ==========================================================================
#
# System-Maintenance
# Copyright (C) 2013-2025 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 -eu

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

# shellcheck disable=SC1091
. gettext.sh


# ###### Usage ##############################################################
usage () {
   echo 2>&1 "$(gettext "Usage:") $0 [-k|--interactive-kernel-removal] [-h|--help]"
   exit 1
}


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

# ====== Handle arguments ===================================================
GETOPT="$(PATH=/usr/local/bin:${PATH} which getopt)"
options="$(${GETOPT} -o kh --long interactive-kernel-removal,help -a -- "$@")"
# shellcheck disable=SC2181
if [[ $? -ne 0 ]]; then
   usage
fi

INTERACTIVE_KERNEL_REMOVAL=0
eval set -- "${options}"
while [ $# -gt 0 ] ; do
   case "$1" in
      -k | --interactive-kernel-removal)
         INTERACTIVE_KERNEL_REMOVAL=1
         shift
         ;;
      -h | --help)
         usage
         # shift
         ;;
      --)
         shift
         break
         ;;
  esac
done
if [ $# -ne 0 ] ; then
   usage
fi

trap 'echo ; echo "Aborted." ; exit' INT

# ====== Find distribution variant ==========================================
if [ ! -e /etc/os-release ] ; then
   gettext >&2 "ERROR: /etc/os-release not found!"
   echo >&2
   exit 1
fi
. /etc/os-release

# ====== Stop AutoFS ========================================================
if [ -e /etc/init.d/autofs ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Stopping AutoFS service ...")\x1b[0m"
   sudo service autofs stop >/dev/null 2>&1 || true
fi

# ====== dpkg package fixes =================================================
if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Applying package fixes, if necessary ...")\x1b[0m"
   sudo DEBIAN_FRONTEND=noninteractive dpkg --configure -a | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log
fi

# ====== Update package repositories ========================================
if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Updating package repositories ...")\x1b[0m"
   sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
elif [ "${ID}" == "freebsd" ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Updating package repositories ...")\x1b[0m"
   sudo env PAGER=cat freebsd-update fetch || true
   sudo env ASSUME_ALWAYS_YES=yes pkg-static update
   if [ -x /usr/sbin/portsnap ] ; then
      sudo /usr/sbin/portsnap -I fetch
   fi
fi

# ====== apt-get package fixes ==============================================
if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Installing missing dependencies, if necessary ...")\x1b[0m"
   sudo DEBIAN_FRONTEND=noninteractive nohup apt-get install -f -y -q \
      -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" 2>&1 | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log
fi

# ====== system update ======================================================
echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Installing updates ...")\x1b[0m"
if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
   # ------ Download --------------------------------------
   sudo DEBIAN_FRONTEND=noninteractive nohup apt-get dist-upgrade -yd -q \
      -o APT::Get::Always-Include-Phased-Updates=true \
      -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" 2>&1 | sudo tee /tmp/install.log &
   wait

   # ------ Update special packages -----------------------
   for package in nornet-server nornet-tunnelbox ; do
      if dpkg -l $package >/dev/null 2>&1 ; then
         if [ "$(find /var/cache/apt/archives/ -name "$package*.deb")" != "" ] ; then
            echo "Updating $package package ..."
            # Do not restart the service on update, in order to avoid
            # unnecessary downtime. Needs a manual restart, if necessary!
            (
               echo "#!/bin/sh"
               echo "exit 101"
            ) | sudo tee /usr/sbin/policy-rc.d >/dev/null
            sudo chmod +x /usr/sbin/policy-rc.d
            sudo DEBIAN_FRONTEND=noninteractive nohup apt-get install --only-upgrade \
               -o APT::Get::Always-Include-Phased-Updates=true \
               -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" \
              $package -y -q 2>&1 | sudo tee /tmp/install.log &
            wait
            echo "Updated $package package."
         fi
      fi
   done

   # Ensure to remove modified policy-rc.d!
   sudo rm -f /usr/sbin/policy-rc.d

   # ------ Update the rest -------------------------------
   sudo DEBIAN_FRONTEND=noninteractive nohup apt-get dist-upgrade -y -q \
      -o APT::Get::Always-Include-Phased-Updates=true \
      -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" 2>&1 | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log
elif [ "${ID}" == "fedora" ] ; then
   ( sudo dnf clean all && sudo dnf upgrade -y ) | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log
elif [ "${ID}" == "freebsd" ] ; then
   ( sudo freebsd-update install ; sudo env ASSUME_ALWAYS_YES=yes pkg-static upgrade -y
     if [ -x /usr/sbin/portsnap ] ; then
        sudo /usr/sbin/portsnap -I update
     fi
   ) | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log
fi

# ====== Auto-remove ========================================================
echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Cleaning up ...")\x1b[0m"
if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
   # ------ Remove unnecessary dependencies ---------------
   sudo DEBIAN_FRONTEND=noninteractive nohup apt-get autoremove -y -q \
      -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" 2>&1 | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log

   # ------ Purge all previously-removed packages ---------
   dpkg --get-selections | awk '$2=="deinstall" { print $1 }' | xargs --no-run-if-empty sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y \
      -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log

   # ------ Clean up the package cache --------------------
   sudo DEBIAN_FRONTEND=noninteractive apt-get clean

   # ------ Clean up /var/crash ---------------------------
   if [ -d /var/crash/ ] ; then
      find /var/crash/ -name "*.crash" -exec sudo rm -f {} \;
   fi
elif [ "${ID}" == "fedora" ] ; then
   sudo DEBIAN_FRONTEND=noninteractive nohup dnf autoremove -y 2>&1 | sudo tee /tmp/install.log &
   wait
   cat /tmp/install.log
   sudo dnf clean all
elif [ "${ID}" == "freebsd" ] ; then
   sudo env ASSUME_ALWAYS_YES=yes pkg-static autoremove -y
   sudo env ASSUME_ALWAYS_YES=yes pkg-static clean all -y
fi

# ====== Remove obsolete kernel packages ====================================
if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Removing obsolete kernels ...")\x1b[0m"

   kernelType=$(uname -r | sed 's/^\([0-9]*\.[0-9]*\.[0-9]*\|[0-9]*\.[0-9]*\.[0-9]*[-+][^-]*\)-\([a-zA-Z0-9\.+]*\)/\2/')
   kernelBootVersion=$(uname -r | sed "s/-${kernelType}$//")
   kernelDiskVersion=$(find /boot -name "vmlinuz-*-${kernelType}" | sort -rV | head -n1 | sed -e "s/^\/boot\/vmlinuz-//" -e "s/-${kernelType}$//")

   echo "$(gettext "Kernel Type:        ") ${kernelType}"
   echo "$(gettext "Kernel Boot Version:") ${kernelBootVersion}"
   echo "$(gettext "Kernel Disk Version:") ${kernelDiskVersion}"

   # The version will be used in regexp: "+" => "\\+"!
   kernelBootVersion="${kernelBootVersion/+/\\+}"
   kernelDiskVersion="${kernelDiskVersion/+/\\+}"

   packageVersionsFile="$(mktemp)"
   apt-show-versions | awk '{ print $1 }' | sed -e "s/:.*$//g" -e "s/\/.*$//g" | sort >"${packageVersionsFile}"

   packagesToRemove="
      nornet-
      linux-image-
      linux-headers-
      linux-modules-
      linux-objects-
"
   packagesToKeep="
      nornet-

      linux-headers-${kernelBootVersion}
      linux-image-${kernelBootVersion}-${kernelType}
      linux-image-extra-${kernelBootVersion}-${kernelType}
      linux-modules-${kernelBootVersion}-${kernelType}
      linux-modules-.*-${kernelBootVersion}-${kernelType}
      linux-objects-.*-${kernelBootVersion}-${kernelType}

      linux-headers-${kernelDiskVersion}
      linux-image-${kernelDiskVersion}-${kernelType}
      linux-image-extra-${kernelDiskVersion}-${kernelType}
      linux-modules-${kernelDiskVersion}-${kernelType}
      linux-modules-.*-${kernelDiskVersion}-${kernelType}
      linux-objects-.*-${kernelDiskVersion}-${kernelType}

      linux-headers-${kernelType}
      linux-image-${kernelType}
      linux-image-extra-${kernelType}
      linux-modules-nvidia-.*-${kernelType}

      linux-headers-virtual
      linux-image-virtual
      linux-image-extra-virtual
"

   toBeRemoved=$(
      while read -r line ; do
         mayBeRemoved=0
         for packageToRemove in ${packagesToRemove} ; do
            if [[ ${line} =~ ^${packageToRemove}.*$ ]] ; then
               mayBeRemoved=1
               break
            fi
         done
         if [ ${mayBeRemoved} -eq 1 ] ; then
            toBeKept=0
            for packageToKeep in ${packagesToKeep} ; do
               if [[ ${line} =~ ^${packageToKeep}.*$ ]] ; then
                  # echo >&2 "Keep: ${line}"
                  echo -n "${line}+ "   # Ensure that this package is KEPT!
                  toBeKept=1
                  break
               fi
            done
            if [ ${toBeKept} -eq 0 ] ; then
               # echo >&2 "Remove: ${line}"
               echo -n "${line} "
            fi
        fi
      done <"${packageVersionsFile}"
   )

   rm -f "${packageVersionsFile}"

   if [ "$(echo "${toBeRemoved}" | xargs -n1 | grep -v ".*+$")" != "" ] ; then

      echo -e "$(gettext "To be removed:")\x1b[31m"
      echo "${toBeRemoved}" | xargs -n1 | grep -v ".*+$" || true
      echo -en "\x1b[0m"
      echo -e "$(gettext "To be kept:")\x1b[32m"
      echo "${toBeRemoved}" | xargs -n1 | grep ".*+$" | sed -e "s/+$//g" || true
      echo -en "\x1b[0m"

      if [ $INTERACTIVE_KERNEL_REMOVAL -eq 0 ] ; then
         # shellcheck disable=SC2086
         sudo DEBIAN_FRONTEND=noninteractive nohup apt-get autoremove -y -qq --purge \
            -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" \
            ${toBeRemoved} 2>&1 | sudo tee /tmp/install.log &
         wait
         cat /tmp/install.log
      else
         # shellcheck disable=SC2086
         sudo apt-get autoremove --purge \
            -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" \
            ${toBeRemoved}
      fi

   else
      echo "Nothing to do!"
   fi
fi

# ====== Refresh snaps ======================================================
if [ -x /usr/bin/snap ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Refreshing snaps ...")\x1b[0m"
   sudo /usr/bin/snap refresh || true
fi

# ====== Firmware updates ===================================================
if [ -x /usr/bin/fwupdmgr ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Checking for firmware updates ...")\x1b[0m"
   sudo /usr/bin/fwupdmgr update || true
fi

# ====== apt-file cache update ==============================================
echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Updating caches ...")\x1b[0m"
if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
   if [ -x /usr/bin/apt-file ] ; then
      sudo /usr/bin/apt-file update >/dev/null
      sudo apt-show-versions -i
   fi
fi
if [ "${ID}" != "freebsd" ] ; then
   if [ -x /usr/bin/updatedb ] ; then
      sudo updatedb
   fi
else
   sudo /etc/periodic/weekly/310.locate
   if [ -x /usr/local/bin/geoipupdate.sh ] ; then
      sudo /usr/local/bin/geoipupdate.sh
   fi
fi

# ====== Start AutoFS =======================================================
if [ -e /etc/init.d/autofs ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Starting AutoFS service ...")\x1b[0m"
   sudo service autofs start >/dev/null 2>&1 || true
fi

# ====== Fix time ===========================================================
if [ -x /usr/sbin/ntpdate ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Synchronising system time ...")\x1b[0m"
   if [ -e /etc/ntp.conf ] ; then
      for ntpServiceName in ntp ntpd ; do
         sudo service ${ntpServiceName} stop >/dev/null 2>&1 || true
      done
   fi

   # Sychronise time from a reliable NTP server:
   # 1. Try Universitetet i Oslo (UiO) NTP servers.
   # 2. Try Physikalisch-Technische Bundesanstalt (PTB) NTP servers.
   # 3. Try ISC NTP server (clock.isc.org).
   # 4. Try Microsoft as last resort, which is likely allowed in a firewall.
   sudo ntpdate ntp2.uio.no || sudo ntpdate ntp1.uio.no || \
      sudo ntpdate ntp2.ptb.de || sudo ntpdate ntp1.ptb.de || \
      sudo ntpdate clock.isc.org || \
      sudo ntpdate time.windows.com || \
      true

   # Write time to hardware clock
   if [ "${ID}" != "freebsd" ] ; then
      if [ -x /usr/sbin/hwclock ] ; then
         sudo hwclock -w
      fi
   else
      sudo date -v +0S
   fi

   if [ -e /etc/ntp.conf ] ; then
      for ntpServiceName in ntp ntpd ; do
         sudo service ${ntpServiceName} start >/dev/null 2>&1 || true
      done
   fi
fi

# ====== SSD trimming =======================================================
if [ -x /sbin/fstrim ] || [ -x /usr/sbin/fstrim ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Running fstrim ...")\x1b[0m"
   sudo fstrim -a -v 2>/dev/null || ( sudo fstrim -v / && sudo fstrim -v /home ) || true
fi
if [ -x /sbin/zpool ] || [ -x /usr/sbin/zpool ] ; then
   echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Running zpool trim ...")\x1b[0m"
   zpools=$(zpool list -H | cut -f1)
   for zpool in ${zpools} ; do
      echo "${zpool}"
      sudo zpool trim "${zpool}" || true
   done
fi

# ====== Done! ==============================================================
echo -e "\x1b[34m$(date +%FT%H:%M:%S): $(gettext "Done!")\x1b[0m"

# ====== Finally, show project package versions =============================
filterScripts=""
for directory in /etc/system-maintenance.d /usr/local/etc/system-maintenance.d ; do
   if [ -d ${directory} ] ; then
      filterScripts=$(find ${directory} -maxdepth 1 -type f -name "[0-9][0-9]*[^~]" | sort)
   fi
done
if [ "${filterScripts}" != "" ] ; then
   fullPackageList=$(mktemp)

   if [ "${ID}" == "debian" ] || [ "${ID}" == "ubuntu" ] ; then
      if [ -x /usr/bin/apt-show-versions ] ; then
         /usr/bin/apt-show-versions | grep -v "not installed$" >"${fullPackageList}" || true
      fi
   elif [ "${ID}" == "fedora" ] ; then
      rpm -qa | sort >"${fullPackageList}" || true
   elif [ "${ID}" == "freebsd" ] ; then
      pkg info --all | sort  >"${fullPackageList}"|| true
   fi

   if [ -e "${fullPackageList}" ] ; then
      for filterScript in ${filterScripts} ; do
         ${filterScript} "${fullPackageList}"
      done
      rm -f "${fullPackageList}"
      echo "=============================================================================="
   fi
fi
