#!/bin/bash

set -eu
set -o pipefail

device_supports_discard=false
ignore_send_error=false
request_confirmation=true
use_secure_delete=true
step=0
do_reset_puavo_os=false
do_reset_windows=false
os_targets='PuavoOS'

# This basically assumes logstep is called only from the top-level,
# which is quite reasonable assumption in this script. But note that
# this is a hack and is not universal. It can get broken if called
# inside a loop or from a function. But for our purposes, I think it's
# quite nice way to report progress.
max_steps=$(grep -c -x logstep "$0")

logmsg() {
  printf "> %s: %s\n" "$1" "$2" >&2 || true
  logger -t puavo-reset-laptop-to-factory-defaults -p "user.${1}" "$2" || true
}

logerr() {
  logmsg error "$1"
}

logstep() {
    step=$((step + 1)) || true
    logmsg info "step ${step}/${max_steps}" || true
}

has_windows() {
  [ -e '/images/boot/.puavo_windows_partition' ]
}

destroy_in_dir() {
  local delete_file_cmd directory force_insecure message preserve_etc_puavo

  force_insecure=false
  preserve_etc_puavo=false
  while true; do
    case "$1" in
      --force-insecure)     force_insecure=true;     shift;;
      --preserve-etc-puavo) preserve_etc_puavo=true; shift;;
      *) break ;;
    esac
  done

  directory=$1
  message=$2

  if [ ! -e "$directory" ]; then
    return 0
  fi

  if ! $device_supports_discard && ! $force_insecure && $use_secure_delete; then
    delete_file_cmd='shred -n 1 -u'
  else
    delete_file_cmd='rm -f'
  fi

  {
    if $preserve_etc_puavo; then
      find "$directory" -xdev -mindepth 5 ! -path '*/etc/puavo' \
        -type f -print0 || return 1
    else
      find "$directory" -xdev -mindepth 1 -type f -print0 || return 1
    fi | { xargs -0 --no-run-if-empty $delete_file_cmd || return 1; }

    if $preserve_etc_puavo; then
      find "$directory" -xdev -mindepth 5 -maxdepth 5 ! -path '*/etc/puavo' \
        -print0 || return 1
    else
      find "$directory" -xdev -mindepth 1 -maxdepth 1 -print0 || return 1
    fi | { xargs -0 --no-run-if-empty rm --one-file-system -rf || return 1; }
  } | pv -F ">>> $message %t" > /dev/null
}

usage() {
  cat <<EOF
$(basename $0) [--force] [--ignore-send-error] [--insecure] [--os-targets=OS_TARGETS]

  --force                  no questions asked
  --ignore-send-error      ignore errors when sending reset state to Puavo
  --insecure               be as fast as possible at the expense of security
  --os-targets=OS_TARGETS  comma-separated list of operating systems to reset,
                           defaults to '${os_targets}', available choices {PuavoOS, Windows}
EOF
  exit 1
}

send_state_reset_to_puavo() {
  local hostname reset_json reset_override_path url

  reset_override_path=$1

  hostname=$(cat /etc/puavo/hostname)                      || return 1
  reset_json=$(jq '{ "reset": . }' "$reset_override_path") || return 1

  url="/v3/devices/${hostname}/state_update"

  echo -n '>>> Sending reset information to Puavo... '
  timeout -k 5 30 puavo-rest-request "$url" --post --user-etc --writable \
    -- --data-binary "$reset_json" -H 'Content-Type: application/json' \
    > /dev/null || return 1
  echo DONE.
}

write_reset_override() {
  local current_time operation reset_override_path user

  operation=$1
  reset_override_path=$2

  current_time=$(date -Iseconds) || return 1
  user="${SUDO_USER:-}"
  if [ -z "$user" ]; then user="${USER:-}"; fi
  if [ -z "$user" ]; then user='?'        ; fi

  jq -r --arg current_time "$current_time" \
        --arg operation    "$operation"    \
        --arg user         "$user" '
    if (.|has("reset")) and (.reset|type == "object")
      and ((.reset|has("request-fulfilled")|not)
              or (.reset["request-fulfilled"] == null)) then
        .reset
    else
      {
        "from":         $user,
        "operation":    $operation,
        "request-time": $current_time,
      }
    end | .["request-fulfilled"] = $current_time
  ' /state/etc/puavo/device.json > "${reset_override_path}.tmp" || return 1
  mv "${reset_override_path}.tmp" "$reset_override_path" || return 1
}

if [ "$(id -u)" -ne 0 ]; then
  echo 'You can run me as root only!' >&2
  exit 1
fi

if [ "$(puavo-conf puavo.mounts.nethomes.enabled)" = 'true' ]; then
  echo 'Refusing to do anything as network home directories are enabled' >&2
  exit 1
fi

for mnt in /home /images /state; do
  if ! mountpoint -q "$mnt"; then
    logerr "${mnt} is not mounted, not continuing"
    exit 1
  fi
done

puavo_hosttype=$(cat /etc/puavo/hosttype)

if [ "$puavo_hosttype" != "laptop" ]; then
  logerr "wiping hosts of type '${puavo_hosttype}' is not supported"
  exit 1
fi

if ! args=$(getopt -n "$0" -o + -l 'force,ignore-send-error,insecure,os-targets:' -- "$@"); then
  usage
fi

eval "set -- $args"
while [ $# -ne 0 ]; do
  case "$1" in
    --force)             request_confirmation=false; shift ;;
    --ignore-send-error) ignore_send_error=true;     shift ;;
    --insecure)          use_secure_delete=false;    shift ;;
    --os-targets)
      shift
      IFS=',' read -ra os_targets <<<"$1"
      shift
      ;;
    --) shift; break ;;
    *)  usage ;;
  esac
done

for os_target in "${os_targets[@]}"; do
  case "${os_target}" in
    PuavoOS)
      do_reset_puavo_os=true
      ;;
    Windows)
      if ! has_windows; then
        logerr 'Windows is not installed'
        exit 1
      fi
      do_reset_windows=true
      ;;
    *)
      logerr "invalid operating system target '${os_target}'"
      usage
      ;;
  esac
done

[ $# -eq 0 ] || usage

logstep
if $do_reset_windows; then
  prwflags=()
  if ! $request_confirmation; then
    prwflags+=('--force')
  fi
  logmsg notice 'starting Windows reset'
  puavo-reset-windows "${prwflags[@]}"
else
  if has_windows; then
    logmsg notice 'skipped Windows reset (Windows is left intact)'
  else
    logmsg notice 'skipped Windows reset (no Windows installation on this host)'
  fi
fi

if ! $do_reset_puavo_os; then
  logmsg notice 'skipped Puavo OS reset'
  # Because Puavo OS reset is skipped, log all remaining steps before
  # exiting to keep the outputted step count of this script always
  # constant, no matter what operating systems are reset.
  for skipped_step in $(seq "$((step + 1))" "${max_steps}"); do
    logstep
  done
  exit 0
fi

# Puavo OS reset operation starts from here.

if $use_secure_delete; then
  operation='reset'
else
  operation='fast-reset'
fi

while $request_confirmation; do
  cat <<'EOF'
DANGER, WILL ROBINSON!  DANGER!

THIS PROCEDURE WILL DESTROY ALL YOUR DATA IN YOUR HOME DIRECTORY!

This utility tries to quickly reset laptop state to something as if
it came from "the factory", as if nobody had ever used it.  This will
DESTROY YOUR STUFF and home directory contents, though in a quick way so
that old files MAY still recoverable with suitable tools.  If possible,
you should consider erasing things in a more secure way by doing a
reinstall with a full disk device "wipe".  That option is not perfect
either, but much better than this quick-and-dirty procedure.

DANGER!
THIS PROCEDURE WILL DESTROY ALL YOUR DATA IN YOUR HOME DIRECTORY, AND MORE!
WHEN IN DOUBT, TURN OFF THE MACHINE, PANIC AND RUN AWAY!
EOF
  echo
  read -p 'Are you sure you want to continue? (yes/no) ' answer
  case "$answer" in
    yes)
      echo 'THIS WILL DESTROY EVERYTHING!!!  WHAT ARE YOU THINKING?!?!?'
      read -p 'Are you REALLY sure? (yes/no) ' second_answer
      if [ "$second_answer" = "yes" ]; then
        break
      fi
      ;;
    no)
      echo 'Okay then, maybe a wise choice!'
      exit 0
      ;;
    *)
      echo "I do not understand that.\n" >&2
      ;;
  esac
done

resetstatus=0

logmsg notice "starting Puavo OS reset operation (${operation})"
logstep

if [ -b /dev/mapper/puavo-imageoverlays ]; then
  if ! mountpoint -q /imageoverlays; then
    if ! { mkdir -p /imageoverlays \
             && mount /dev/mapper/puavo-imageoverlays /imageoverlays; }; then
      logerr 'could not mount imageoverlays'
      resetstatus=1
    fi
  fi
fi

if [ "$(lsblk -b -n -o DISC-MAX -r /dev/mapper/puavo-home)" != 0 ]; then
  logmsg info 'disk supports discard'
  device_supports_discard=true
else
  logmsg info 'disk does not support discard, operations might be slow'
fi
logstep

## Best-effort killing of some possibly interfering processes
systemctl mask colord.service >/dev/null 2>&1 || true
systemctl stop colord.service >/dev/null 2>&1 || true
systemctl mask docker.service >/dev/null 2>&1 || true
systemctl stop docker.service >/dev/null 2>&1 || true
pkill -f /usr/libexec/tracker   || true
pkill -f /usr/libexec/gvfs      || true
pkill -f /usr/bin/pulseaudio    || true
pkill -f /usr/libexec/evolution || true
pkill -f /usr/libexec/gsd-color || true

cd /

# Remove primary user (so that we will send this information
# to Puavo through update-configuration).
if cat /dev/null > /state/etc/puavo/primary_user_override; then
  logmsg info 'wrote /state/etc/puavo/primary_user_override'
else
  logerr 'could not write /state/etc/puavo/primary_user_override'
  resetstatus=1
fi
logstep

# we can speed up secure delete by not applying it to puavo-pkgs
if destroy_in_dir --force-insecure /images/puavo-pkg/ \
                    'Removing all puavo-pkgs'; then
  logmsg info 'removed all puavo-pkgs'
else
  logerr 'error in removing all puavo-pkgs'
  resetstatus=1
fi
logstep

if destroy_in_dir /home/ 'Cleaning up home directories'; then
  logmsg notice 'cleaned up all home directories'
else
  logerr 'error in cleaning up home directories'
  resetstatus=1
fi
logstep

if destroy_in_dir /state/etc/puavo/local/ \
                    'Removing local configurations (made by user)'; then
  logmsg info 'removed local configurations'
else
  logerr 'error in removing local configurations'
  resetstatus=1
fi
logstep

if destroy_in_dir /state/var/cache/ 'Cleaning up caches'; then
  logmsg info 'cleaned up caches'
else
  logerr 'error in cleaning up caches'
  resetstatus=1
fi
logstep

if destroy_in_dir /state/var/lib/ 'Removing some system configurations'; then
  logmsg info 'removed some system configurations'
else
  logerr 'error in removing some system configurations'
  resetstatus=1
fi
logstep

{ /usr/lib/puavo-ltsp-install/update-configuration || true; } 2>&1 \
  | pv -F '>>> Updating system configuration %t' > /dev/null
logmsg info 'system configuration update done'

if destroy_in_dir --force-insecure --preserve-etc-puavo /imageoverlays/ \
                    'Cleaning up most of /imageoverlays'; then
  logmsg info 'cleaned up most of /imageoverlays'
else
  logerr 'error in cleaning up (most of) /imageoverlays'
  resetstatus=1
fi
logstep

if destroy_in_dir /state/etc/NetworkManager/system-connections/ \
               'Removing network manager connections'; then
  logmsg info 'removed network manager configurations'
else
  logerr 'error in removing network manager configurations'
  resetstatus=1
fi
logstep

reset_override_path='/state/etc/puavo/reset_override'
if [ "$resetstatus" -eq 0 ]; then
  if write_reset_override "$operation" "$reset_override_path"; then
    logmsg info 'wrote reset override'
    if send_state_reset_to_puavo "$reset_override_path"; then
      logmsg notice 'sent reset state to puavo (with success)'
    else
      {
        echo
        echo 'Failed to sending reset information to Puavo.  You may reboot'
        echo 'this host, it tries to update the reset information later,'
        echo 'but you may also remove it manually from the Puavo interface.'
        echo
      } >&2
      if $ignore_send_error; then
        logerr 'failed sending reset state to puavo (ignoring)'
      else
        logerr 'failed sending reset state to puavo'
        resetstatus=1
      fi
    fi
  else
    echo
    echo 'Error in writing reset override' >&2
    echo
    logerr 'error in writing reset override'
    resetstatus=1
  fi
else
  logmsg warning 'not sending reset state to puavo because of failures'
fi
logstep

if destroy_in_dir --force-insecure /imageoverlays/ \
               'Cleaning up all of /imageoverlays'; then
  logmsg info 'cleaned up all of /imageoverlays'
else
  logmsg warning 'error in cleaning up all of /imageoverlays'
  resetstatus=1
fi
logstep

if systemctl start fstrim | pv -F '>>> Running fstrim %t' > /dev/null; then
  logmsg info 'fstrim finished with success'
else
  logerr 'fstrim returned an error'
  resetstatus=1
fi
logstep

if [ "$resetstatus" -ne 0 ]; then
  logerr 'errors occurred in device reset'
  exit $resetstatus
fi

logmsg notice 'device reset finished with success!'

i=5
echo
echo -n "Rebooting in $i seconds..."
while [ "$i" -gt 0 ]; do
  i=$(($i - 1))
  sleep 1
  echo -n " $i"
done
echo .

reboot
