#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

# --- Local configuration -----------------------------------------------------
# The Proton save and profile are the primary state and are always required.
#
# Steam remote copies are optional and are restored only when the selected
# backup actually contains them. Their paths live under a per-account
# userdata directory, so STEAM_USER_ID has no default: it is required for a
# backup that carries Steam remote data, and ignored otherwise.
#
#   STEAM_USER_ID=123456789 ./bg3-honour-restore.sh --latest
#
# Find your numeric ID by listing ~/.local/share/Steam/userdata/.
# BG3_BACKUP_ROOT is optional and overrides where backups are read from.
# The Proton save path is account-independent and needs no configuration.
STEAM_USER_ID="${STEAM_USER_ID:-}"
BACKUP_ROOT="${BG3_BACKUP_ROOT:-$HOME/Documents/misc/bg3-backups}"

PROTON_STORY="$HOME/.local/share/Steam/steamapps/compatdata/1086940/pfx/drive_c/users/steamuser/AppData/Local/Larian Studios/Baldur's Gate 3/PlayerProfiles/Public/Savegames/Story"
PROTON_PUBLIC="$(dirname "$(dirname "$PROTON_STORY")")"
PROTON_PROFILE="$PROTON_PUBLIC/profile8.lsf"
SNAPSHOT_ROOT="$BACKUP_ROOT/_pre_restore_snapshots"

# The Steam remote paths are derived only when an ID is configured, so an
# unset value can never produce a path that looks real. Empty here means
# Steam remote handling is unavailable for this run.
if [[ -n "$STEAM_USER_ID" ]]; then
  STEAM_REMOTE_STORY="$HOME/.local/share/Steam/userdata/$STEAM_USER_ID/1086940/remote/_SAVE_Public/Savegames/Story"
  STEAM_REMOTE_SAVE_PUBLIC="$(dirname "$(dirname "$STEAM_REMOTE_STORY")")"
  STEAM_REMOTE_PROFILE_PUBLIC="$HOME/.local/share/Steam/userdata/$STEAM_USER_ID/1086940/remote/_PROFILE_Public"
  STEAM_REMOTE_PROFILE="$STEAM_REMOTE_PROFILE_PUBLIC/profile8.lsf"
else
  STEAM_REMOTE_STORY=""
  STEAM_REMOTE_SAVE_PUBLIC=""
  STEAM_REMOTE_PROFILE_PUBLIC=""
  STEAM_REMOTE_PROFILE=""
fi

SNAPSHOT_DIR=""
PROTON_TARGET=""
REMOTE_TARGET=""
ROLLBACK_READY=0

usage() {
  cat <<USAGE
Usage:
  $(basename "$0") --list
  $(basename "$0") BACKUP_NAME_OR_PATH [--yes]
  $(basename "$0") --latest [--yes]

Examples:
  $(basename "$0") --list
  $(basename "$0") 20260411-185926__<SAVE_ID>__HonourMode
  $(basename "$0") --latest --yes

Restores:
  - the Honour save directory
  - profile8.lsf from Proton
  - optional Steam remote copies if that backup contains them

Environment:
  STEAM_USER_ID    optional; your numeric Steam userdata directory, as listed
                   under ~/.local/share/Steam/userdata/. Required only when
                   the selected backup contains Steam remote data. Backups
                   without it restore Proton-only and need no ID.
  BG3_BACKUP_ROOT  optional; defaults to $BACKUP_ROOT

Safety behavior:
  - Refuses to restore a backup's Steam remote data without STEAM_USER_ID,
    before any live file is moved.
  - Refuses to run if BG3/Steam Play helper processes look active.
  - Creates a pre-restore safety snapshot of the current live save and profile.
  - Rolls back automatically if restore verification fails.
  - Prompts for confirmation unless --yes is provided.
USAGE
}

# Called only once the selected backup has been inspected and found to carry
# Steam remote data. Proton-only backups never reach it.
require_steam_user_id() {
  [[ -n "$STEAM_USER_ID" ]] && return 0
  cat >&2 <<ERR
ERROR: STEAM_USER_ID is not set, but the selected backup contains Steam
remote data.

The Steam remote save and profile live under a per-account userdata
directory, so restoring them needs your numeric Steam ID. Your ID is one of
the directories under:
  ~/.local/share/Steam/userdata/

Restore with it set, for example:
  STEAM_USER_ID=123456789 $(basename "$0") --latest

No live file has been touched: this check runs before the safety snapshot and
before anything is moved. To restore Proton data only, select a backup
without steam_remote content.
ERR
  exit 1
}

is_game_running() {
  # Matches process NAMES, not command lines.
  #
  # The published script names contain "bg3", so a `pgrep -f` match (which
  # tests the whole command line) flags the running script itself and the
  # guard can never pass. Process names cannot self-match: a shell running
  # this script is named "bash", whatever the script file is called.
  #
  # Names are truncated to 15 characters by the kernel, so every pattern here
  # is short enough to be matchable. wine/wineserver are kept as the broad
  # safety net — any Wine runtime in use means the game may be running.
  pgrep -x 'bg3|bg3\.exe|bg3_dx11\.exe|wine|wine64|wine-preloader|wine64-preload|wineserver|proton|pressure-vessel' >/dev/null 2>&1
}

list_backups() {
  [[ -d "$BACKUP_ROOT" ]] || { echo "No backup directory found at: $BACKUP_ROOT"; return 0; }
  find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d ! -name '.*' ! -name '_pre_restore_snapshots' -printf '%TY-%Tm-%Td %TH:%TM:%TS  %f\n' | sort
}

resolve_backup_dir() {
  local input="${1:-}"
  if [[ -z "$input" ]]; then
    return 1
  elif [[ "$input" == "--latest" ]]; then
    find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d ! -name '.*' ! -name '_pre_restore_snapshots' -printf '%T@ %p\n' \
      | sort -nr | awk 'NR==1 {sub(/^[^ ]+ /, ""); print; exit}'
  elif [[ -d "$input" ]]; then
    printf '%s\n' "$input"
  elif [[ -d "$BACKUP_ROOT/$input" ]]; then
    printf '%s\n' "$BACKUP_ROOT/$input"
  else
    return 1
  fi
}

rollback() {
  local rc=$?
  [[ "$ROLLBACK_READY" -eq 1 ]] || exit "$rc"

  rm -rf -- "$PROTON_TARGET"
  if [[ -d "$SNAPSHOT_DIR/proton/$(basename "$PROTON_TARGET")" ]]; then
    mv -- "$SNAPSHOT_DIR/proton/$(basename "$PROTON_TARGET")" "$PROTON_STORY/"
  fi
  if [[ -f "$SNAPSHOT_DIR/proton/profile8.lsf" ]]; then
    mv -- "$SNAPSHOT_DIR/proton/profile8.lsf" "$PROTON_PROFILE"
  fi

  # Skipped entirely when Steam remote handling is not in play: REMOTE_TARGET
  # is empty then, and an empty target must never reach rm/mv.
  if [[ -n "$REMOTE_TARGET" ]]; then
    rm -rf -- "$REMOTE_TARGET"
    if [[ -d "$SNAPSHOT_DIR/steam_remote/$(basename "$REMOTE_TARGET")" ]]; then
      mkdir -p -- "$STEAM_REMOTE_STORY"
      mv -- "$SNAPSHOT_DIR/steam_remote/$(basename "$REMOTE_TARGET")" "$STEAM_REMOTE_STORY/"
    fi
    if [[ -f "$SNAPSHOT_DIR/steam_remote/profile8.lsf" ]]; then
      mkdir -p -- "$STEAM_REMOTE_PROFILE_PUBLIC"
      mv -- "$SNAPSHOT_DIR/steam_remote/profile8.lsf" "$STEAM_REMOTE_PROFILE"
    fi
  fi

  echo "ERROR: Restore failed. Previous live files were rolled back from $SNAPSHOT_DIR" >&2
  exit "$rc"
}
trap rollback ERR INT TERM

main() {
  local confirm=no target_arg="" backup_dir=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --help|-h)
        usage
        exit 0
        ;;
      --list)
        list_backups
        exit 0
        ;;
      --latest)
        target_arg="--latest"
        shift
        ;;
      --yes)
        confirm=yes
        shift
        ;;
      -*)
        echo "ERROR: Unknown option: $1" >&2
        usage >&2
        exit 1
        ;;
      *)
        [[ -z "$target_arg" ]] || { echo "ERROR: Too many arguments." >&2; usage >&2; exit 1; }
        target_arg="$1"
        shift
        ;;
    esac
  done

  [[ -n "$target_arg" ]] || { usage; exit 1; }

  # STEAM_USER_ID is not checked here: --help and --list must work without it,
  # and a Proton-only backup does not need it at all. The requirement is
  # decided below, once the selected backup has actually been inspected.

  [[ -d "$PROTON_STORY" ]] || { echo "ERROR: Proton save directory not found: $PROTON_STORY" >&2; exit 1; }
  mkdir -p "$BACKUP_ROOT" "$SNAPSHOT_ROOT" "$PROTON_STORY" "$PROTON_PUBLIC"

  if is_game_running; then
    cat >&2 <<ERR
ERROR: BG3 or related Wine/Proton processes appear to be running.
For a safe restore, close the game completely and run this script again.
ERR
    exit 1
  fi

  backup_dir=$(resolve_backup_dir "$target_arg") || {
    echo "ERROR: Backup not found: $target_arg" >&2
    echo >&2
    echo "Available backups:" >&2
    list_backups >&2
    exit 1
  }

  local proton_backup_root proton_run_dir run_name proton_backup_profile
  local remote_backup_dir remote_backup_profile remote_has_run remote_has_profile
  local proton_tmp_dir remote_tmp_dir proton_profile_tmp remote_profile_tmp now

  proton_backup_root="$backup_dir/proton"
  [[ -d "$proton_backup_root" ]] || { echo "ERROR: Invalid backup: missing proton/ directory." >&2; exit 1; }

  proton_run_dir=$(find "$proton_backup_root" -mindepth 1 -maxdepth 1 -type d -name '*__HonourMode' | head -n1)
  [[ -n "$proton_run_dir" ]] || { echo "ERROR: Invalid backup: no HonourMode run found under proton/." >&2; exit 1; }
  [[ -f "$proton_run_dir/HonourMode.lsv" ]] || { echo "ERROR: Invalid backup: missing proton HonourMode.lsv." >&2; exit 1; }

  proton_backup_profile="$proton_backup_root/profile8.lsf"
  [[ -f "$proton_backup_profile" ]] || { echo "ERROR: Invalid backup: missing proton profile8.lsf." >&2; exit 1; }

  run_name=$(basename "$proton_run_dir")
  remote_backup_dir="$backup_dir/steam_remote/$run_name"
  remote_backup_profile="$backup_dir/steam_remote/profile8.lsf"
  PROTON_TARGET="$PROTON_STORY/$run_name"
  REMOTE_TARGET=""
  remote_has_run=no
  remote_has_profile=no
  [[ -d "$remote_backup_dir" ]] && remote_has_run=yes
  [[ -f "$remote_backup_profile" ]] && remote_has_profile=yes

  # The ID is required only when this backup actually carries Steam remote
  # data. This sits after the backup has been inspected but before the
  # confirmation prompt, the safety snapshot and the first move — so a missing
  # ID fails here with the live filesystem untouched and nothing to unwind.
  if [[ "$remote_has_run" == yes || "$remote_has_profile" == yes ]]; then
    require_steam_user_id
    REMOTE_TARGET="$STEAM_REMOTE_STORY/$run_name"
  fi

  echo "Backup selected : $backup_dir"
  echo "Run to restore  : $run_name"
  echo "Proton target   : $PROTON_TARGET"
  echo "Proton profile  : $PROTON_PROFILE"
  echo "Steam remote run: $remote_has_run"
  echo "Steam remote lsf: $remote_has_profile"
  echo

  if [[ "$confirm" != "yes" ]]; then
    read -r -p "Proceed with restore? [y/N] " answer
    case "$answer" in
      y|Y|yes|YES) ;;
      *) echo "Restore cancelled."; exit 0 ;;
    esac
  fi

  now=$(date '+%Y%m%d-%H%M%S')
  # mktemp creates the directory atomically and answers with the name it
  # actually created, so two restores started within the same second can no
  # longer collide on a second-resolution timestamp. The timestamp and run
  # name stay as the visible prefix; only the suffix is random.
  SNAPSHOT_DIR=$(mktemp -d "$SNAPSHOT_ROOT/${now}__${run_name}.XXXXXX")
  mkdir -p "$SNAPSHOT_DIR/proton" "$SNAPSHOT_DIR/steam_remote"

  if [[ -e "$PROTON_TARGET" ]]; then
    mv -- "$PROTON_TARGET" "$SNAPSHOT_DIR/proton/"
  fi
  if [[ -f "$PROTON_PROFILE" ]]; then
    mv -- "$PROTON_PROFILE" "$SNAPSHOT_DIR/proton/"
  fi
  if [[ -e "$REMOTE_TARGET" ]]; then
    mv -- "$REMOTE_TARGET" "$SNAPSHOT_DIR/steam_remote/"
  fi
  if [[ -f "$STEAM_REMOTE_PROFILE" ]]; then
    mkdir -p -- "$STEAM_REMOTE_PROFILE_PUBLIC"
    mv -- "$STEAM_REMOTE_PROFILE" "$SNAPSHOT_DIR/steam_remote/"
  fi

  ROLLBACK_READY=1

  proton_tmp_dir="$PROTON_STORY/.${run_name}.restore_tmp.$$"
  proton_profile_tmp="$PROTON_PUBLIC/.profile8.lsf.restore_tmp.$$"
  rm -rf -- "$proton_tmp_dir"
  cp -a -- "$proton_run_dir" "$proton_tmp_dir"
  cp -a -- "$proton_backup_profile" "$proton_profile_tmp"
  [[ -f "$proton_tmp_dir/HonourMode.lsv" ]] || { echo "ERROR: Proton temp restore missing HonourMode.lsv." >&2; exit 1; }
  [[ -f "$proton_profile_tmp" ]] || { echo "ERROR: Proton temp restore missing profile8.lsf." >&2; exit 1; }
  mv -- "$proton_tmp_dir" "$PROTON_TARGET"
  mv -- "$proton_profile_tmp" "$PROTON_PROFILE"

  if [[ "$remote_has_run" == yes ]]; then
    mkdir -p -- "$STEAM_REMOTE_STORY"
    remote_tmp_dir="$STEAM_REMOTE_STORY/.${run_name}.restore_tmp.$$"
    rm -rf -- "$remote_tmp_dir"
    cp -a -- "$remote_backup_dir" "$remote_tmp_dir"
    [[ -f "$remote_tmp_dir/HonourMode.lsv" ]] || { echo "ERROR: Steam remote temp restore missing HonourMode.lsv." >&2; exit 1; }
    mv -- "$remote_tmp_dir" "$REMOTE_TARGET"
  fi

  if [[ "$remote_has_profile" == yes ]]; then
    mkdir -p -- "$STEAM_REMOTE_PROFILE_PUBLIC"
    remote_profile_tmp="$STEAM_REMOTE_PROFILE_PUBLIC/.profile8.lsf.restore_tmp.$$"
    cp -a -- "$remote_backup_profile" "$remote_profile_tmp"
    [[ -f "$remote_profile_tmp" ]] || { echo "ERROR: Steam remote temp restore missing profile8.lsf." >&2; exit 1; }
    mv -- "$remote_profile_tmp" "$STEAM_REMOTE_PROFILE"
  fi

  [[ -f "$PROTON_TARGET/HonourMode.lsv" ]] || { echo "ERROR: Restore verification failed: proton HonourMode.lsv not found after restore." >&2; exit 1; }
  [[ -f "$PROTON_PROFILE" ]] || { echo "ERROR: Restore verification failed: proton profile8.lsf not found after restore." >&2; exit 1; }

  ROLLBACK_READY=0
  trap - ERR INT TERM

  cat <<DONE
Restore completed successfully.
Restored backup:
  $backup_dir

Safety snapshot of previous live save and profile:
  $SNAPSHOT_DIR
DONE
}

main "$@"
