#!/bin/bash

## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

#set -x

true "$0: START"

set -o errexit
set -o nounset
set -o errtrace
set -o pipefail

source /usr/libexec/helper-scripts/light_sleep.bsh

system_ready_check_function(){
  system_ready_check_output="$(leaprun system-ready-check 2>&1)"
}

leaprun_availability_test() {
  ## sets: use_leaprun
  #source /usr/libexec/helper-scripts/use_leaprun.sh
  ## also does source above script
  source /usr/libexec/helper-scripts/pkg_manager_running_check

  if [ "$use_leaprun" = "no" ]; then
    return 1
  fi
  return 0
}

leaprun_check_function() {
  leaprun_check_output="$(leaprun --check -- apt-get-update 2>&1)"
}

sdwdate_ready() {
  if [ -e /run/sdwdate/success ]; then
    return 0
  fi
  if [ -e /run/sdwdate/first_success ]; then
    return 0
  fi
  return 1
}

onion_time_pre_script_run() {
  onion_time_pre_script_output="$(leaprun onion-time-pre-script 2>&1)"
}

## run_with_attempts <attempts> <sleep> <label> <command...>
run_with_attempts() {
  local attempts sleep_amount label
  attempts="$1"
  sleep_amount="$2"
  label="$3"
  shift 3

  local i
  for ((i=1; i<=attempts; i++)); do
    true "$0: INFO: $label iteration: $i"
    if "$@"; then
      true "$0: INFO: $label - success. Proceeding."
      return 0
    fi
    if [ "$i" -ge "$attempts" ]; then
      true "$0: INFO: $label - failed. Giving up after $attempts attempts."
      return 1
    fi
    true "$0: INFO: $label - failed. Sleep and try again."
    light_sleep "${sleep_amount}" &
  done
}

close_notification() {
  true "$0: $FUNCNAME: START"
  ## notification_id might be unset if signal sigterm is received very quickly.
  [[ -v notification_id ]] || notification_id=""
  if [ "$notification_id" = "" ]; then
    true "$0: $FUNCNAME: notification_id is unset. return 0"
    return 0
  fi

  qdbus \
    org.freedesktop.Notifications \
    /org/freedesktop/Notifications \
    org.freedesktop.Notifications.CloseNotification \
    "$notification_id"

  true "$0: $FUNCNAME: END"
}

send_notification_wait_exit() {
  local sleep_milliseconds
  ## 6 hours
  sleep_milliseconds=$(( $sleep_seconds * 1000 ))
  ## Debugging.
  #sleep_milliseconds=1000

  local title
  title="updatecheck: $1"
  local message
  message="$2"
  local suppress_systemcheck_info
  suppress_systemcheck_info="${3:-}"
  if [ "$suppress_systemcheck_info" != 'yes' ]; then
    message+="
To view log, run:

journalctl --boot --user -u updatecheck.service

Please run systemcheck."
  fi

  printf "%s\n" "$0: $title: $message"

  ## If not using '--wait' there is no feedback when the notification has been dismissed.
  ## If using '--wait' we don't get the notification ID.
  ## If launching 'notify-send' into the background, we cannot capture the notification ID.
  #notification_id="$(notify-send --wait --print-id --expire-time="$sleep_milliseconds" --app-name='updatecheck' -- "$title" "$message")" &

  ## Launch notify-send in a background coprocess so we can
  ## 'read' the ID straight away but still let '--wait' keep running.
  coproc notify_send_coproc {
    stdbuf \
      --output=L \
      -- \
        notify-send \
          --wait \
          --print-id \
          --expire-time="100000" \
          --app-name='updatecheck' \
          -- \
          "$title" \
          "$message"
  }

  coproc_stdout_fd="${notify_send_coproc[0]}"
  coproc_stderr_fd="${notify_send_coproc[1]}"

  test -e "/proc/$$/fd/${coproc_stdout_fd}"
  test -e "/proc/$$/fd/${coproc_stderr_fd}"

  true "INFO: read -r notification_id <& ${coproc_stdout_fd}"
  read -r notification_id <& "${coproc_stdout_fd}"
  true "INFO: notification_id: $notification_id"

  true "INFO: exec {coproc_stdout_fd}>&-"
  exec {coproc_stdout_fd}>&-
  true "INFO: exec {coproc_stderr_fd}>&-"
  exec {coproc_stderr_fd}>&-

  ! test -e "/proc/$$/fd/${coproc_stdout_fd}"
  ! test -e "/proc/$$/fd/${coproc_stderr_fd}"

  ## 'coproc' sets variable notify_send_coproc_PID.
  true "INFO: notify_send_coproc_PID: $notify_send_coproc_PID"
  true "INFO: Waiting for notify_send_coproc_PID..."
  wait "$notify_send_coproc_PID" || true "INFO: coproc exit code: $?"
  true "INFO: Done waiting for notify_send_coproc_PID."

  ## Exit in case no signal was received and notification has been dismissed.
  exit 0
}

sigterm_trap() {
  true "$0: $FUNCNAME: START"
  close_notification
  true "$0: $FUNCNAME: END"
  exit 0
}

exit_trap() {
  true "$0: $FUNCNAME: end of script: END"
  exit 0
}

update_output_check() {
  ## Example update_output:
  ## > E: Malformed line 1 in source list /etc/apt/sources.list.d/debian.list (type)
  ## > E: The list of sources could not be read.
  ##
  ## Err:4 http://HTTPS///fasttrack.debian.net/debian bullseye-fasttrack InRelease
  ## 500  SSL error: wrong version number [IP: 127.0.0.1 3142]
  if printf '%s' "$update_output" | grep --ignore-case -E -- '^Error:|^E:|^Err:' >/dev/null; then
    printf "%s\n" "$0: WARNING: output of 'leaprun apt-get-update':"
    printf "%s\n" "---------------------------------------------"
    printf "%s\n" "$update_output"
    printf "%s\n" "---------------------------------------------"
    send_notification_wait_exit "Software updates check failure" "Output of command 'leaprun apt-get-update' contained error messages and has been logged to journal."
  fi
}

[[ -v sleep_seconds ]] || sleep_seconds="21600"
[[ -v initial_wait_minutes ]] || initial_wait_minutes="2"

initial_wait_seconds=$(( initial_wait_minutes * 60 ))

trap "sigterm_trap" SIGTERM SIGINT
trap "exit_trap" EXIT

who_ami="$(whoami)"

if ! run_with_attempts 3 "${initial_wait_seconds}" "system ready check" system_ready_check_function; then
  send_notification_wait_exit "Software updates check failure" "'leaprun system-ready-check' failed.

  Debugging information:
  - system_ready_check_output: '$system_ready_check_output'"
fi

if ! run_with_attempts 3 "${initial_wait_seconds}" "leaprun availability test" leaprun_availability_test; then
  send_notification_wait_exit "Software updates check failure" "privleap issue.

  Debugging information:
  - use_leaprun: '$use_leaprun'
  - leaprun_useable_result: '$leaprun_useable_result'"
fi

if ! run_with_attempts 3 "${initial_wait_seconds}" "leaprun functionality test" leaprun_check_function; then
  mkdir --parents -- ~/.systemcheck/status-files
  msg="\
Account '$who_ami' lacks privleap permission. See:
https://www.kicksecure.com/wiki/Systemcheck#updatecheck_for_accounts_other_than_user"
  printf "%s\n" "$0: ERROR: $msg"
  /usr/libexec/msgcollector/one-time-popup ~/.systemcheck/status-files/leap-run-missing-permission "Software updates check failure" "$msg"
  exit 0
fi

if ! run_with_attempts 10 "${initial_wait_seconds}" "non-loopback IP" /usr/libexec/helper-scripts/check-network-access; then
  true "$0: INFO: No non-loopback address found. Do not send an error notification in this instance, the user probably just doesn't have an Internet connection. Therefore exiting."
  exit 0
fi

if ! run_with_attempts 5 "${initial_wait_seconds}" "onion-time-pre-script" onion_time_pre_script_run; then
  printf "%s\n" "$0: INFO: onion_time_pre_script_output: $onion_time_pre_script_output"
  send_notification_wait_exit "Software updates check failure" "Command 'leaprun onion-time-pre-script' failed. Command output has been logged to journal."
fi

if ! run_with_attempts 3 "${initial_wait_seconds}" "sdwdate ready check" sdwdate_ready; then
  send_notification_wait_exit "Software updates check failure" "sdwdate did not complete yet."
fi

if ! run_with_attempts 3 "${initial_wait_seconds}" "check package manager running" check_package_manager_running_helper; then
  true "$0: INFO: check_package_manager_running_helper did not complete yet."
  send_notification_wait_exit "Software updates check failure" "Package update database is locked. Is APT already running?"
fi

## There should be no issue with duplicate pidfile creation in script
## /usr/libexec/helper-scripts/apt-get-update by function 'write_pid_file'.
## This is because above it is being checked, that APT is not already running.
## There is a small risk for TOCTOU.
if ! update_output="$(leaprun apt-get-update 2>&1)" ; then
  update_output_check
  ## 'leaprun apt-get-update' might exit non-zero but update_output might not contain errors.
  printf "%s\n" "$0: ERROR: output of 'leaprun apt-get-update':"
  printf "%s\n" "---------------------------------------------"
  printf "%s\n" "$update_output"
  printf "%s\n" "---------------------------------------------"
  send_notification_wait_exit "Software updates check failure" "Command 'leaprun apt-get-update' failed (non-zero exit code). Command output has been logged to journal."
fi

## 'leaprun apt-get-update' might exit 0 but update_output might still contain errors.
update_output_check

## 'wc' can core dump. Example:
## zsh: illegal hardware instruction (core dumped) wc -l
## https://github.com/rspamd/rspamd/issues/5137
true "$0: INFO: Testing wc command..."
if ! printf '%s\n' '' | wc -l >/dev/null ; then
  send_notification_wait_exit "Software updates check failure" "Command \"printf '%s\n' '' | wc -l\" failed."
fi

true "$0: INFO: Count upgrades using wc command..."
## Cannot use 'apt-get list --upgradable' because it leads to error:
## > E: Command line option --upgradable is not understood in combination with the other options
##
## Using 'printf' with '>/dev/null' with xtrace (set -x) output.
## Use '2>/dev/null' to ignore warning 'WARNING: apt does not have a stable CLI interface. Use with caution in scripts.'
if ! update_package_count="$(LC_ALL=C apt list --upgradable 2>/dev/null | wc -l)" ; then
  send_notification_wait_exit "Software updates check failure" "Command 'apt list --upgradable' failed."
fi

if [[ "$update_package_count" = *[!0-9]* ]]; then
  send_notification_wait_exit "Software updates check failure" "Variable update_package_count is not strictly numeric."
fi

## Remove first counted line by 'apt list --upgradable':
## > Listing... Done
(( update_package_count-- ))

if [ "$update_package_count" = "0" ]; then
   printf "%s\n" "$0: INFO: No updates available."
   exit 0
fi

update_notify_title='Software updates available'
update_notify_body="${update_package_count}"
if (( update_package_count > 1 )); then
   update_notify_body+=' packages have'
else
   update_notify_body+=' package has'
fi
update_notify_body+=' updates available.'

send_notification_wait_exit "${update_notify_title}" "${update_notify_body}" 'yes'
