#!/usr/bin/env bash
# @file installJRMC
# @brief Installs JRiver Media Center and associated services
# @description See installJRMC --help or print_help() below for usage
# Copyright (c) 2021-2026 Bryan C. Roessler
# This software is released under the Apache License.
# https://www.apache.org/licenses/LICENSE-2.0
#
# TODO (v2)
# * Interactive (ncurses) mode
# * Additional containerization (createrepo and rpmbuild)
#
# BUGS
# * No createrepo on Mint
#
# NOTES
# * Be careful with tabs in heredocs
# * Avoid execute() for stdout
# * RPM repo creation requires rpmbuild and rpmsign
#
# Allow indirection to match service names to their functions
# shellcheck disable=SC2329
shopt -s extglob

declare -g SCRIPT_VERSION="1.35.16-dev"
declare -g MC_VERSION_HARDCODE="35.0.74" # do find all replace
declare -g MC_REPO_HARDCODE="bookworm" # should match the MC_VERSION_HARDCODE
declare -g BOARD_ID="92.0" # MC35 board ID for fallback latest version detection
declare -gi SELF_UPDATE_SWITCH=1 # 0 to disable installJRMC self-update
declare -g SCRIPT_URL="https://git.bryanroessler.com/bryan/installJRMC/raw/branch/master/installJRMC" # self-update URL
# declare -g SCRIPT_URL="https://raw.githubusercontent.com/cryobry/installJRMC/refs/heads/master/installJRMC" # backup URL
declare -gi DEBUG=${DEBUG:-0} # set default debug and allow DEBUG env override (default: disabled)

# @description Print help text
print_help() {
  debug "${FUNCNAME[0]}()"

  cat <<-EOF
		SEE:
		  README.md for more information

		USAGE:
		  installJRMC [[OPTION] [VALUE]]...

		  installJRMC defaults to --install=repo on platforms with a JRiver repository and --install=local on others.
		  Specifying --build, --createrepo, --service, or --uninstall disables the default install method.

		OPTIONS
		  --install, -i repo|local
		    repo: Install MC from repository, updates are handled by the system package manager.
		    local: Build and install MC locally from official source package.
		  --build[=suse|fedora|centos|mandriva]
		    Build RPM from source DEB but do not install.
		    Optionally, specify a target distro for cross-building (ex. --build=suse, note the '=').
		  --compat
		    Build/install MC locally without minimum dependency version requirements.
		  --mcversion VERSION
		    Specify the MC version, ex. "$MC_VERSION_HARDCODE" or "${MC_VERSION_HARDCODE%%.*}" (default: latest release).
		  --arch VERSION
		    Specify the target MC architecture, ex. "amd64", "arm64", etc (default: host).
		  --mcrepo REPO
		    Specify the MC repository, ex. "bullseye", "bookworm", "noble", etc (default: auto).
		  --outputdir PATH
		    Generate reusable installJRMC output in this PATH (default: ./output).
		  --restorefile MJR_FILE
		    Restore file location for automatic license registration.
		  --betapass PASSWORD
		    Enter beta team password for access to beta builds.
		  --service, -s SERVICE
		    See SERVICES below for possible services to install.
		  --service-type user|system
		    Starts services at boot (system) or at user login (user) (default: per service, see SERVICES).
		  --no-update
		    Disable automatic installJRMC self-update.
		  --uninstall, -u
		    Uninstall JRiver MC, remove services, containers, and firewall rules (does not remove library files).
		  --yes, -y, --auto
		    Assume yes response to questions.
		  --version, -v
		    Print installJRMC version and exit.
		  --debug, -d
		    Print debug output.
		  --help, -h
		    Print help dialog and exit.

		SERVICES
		  jriver-mediaserver (default --service-type=user)
		    Enable and start a mediaserver systemd service (requires an existing X server).
		  jriver-mediacenter (user)
		    Enable and start a mediacenter systemd service (requires an existing X server).
		  jriver-x11vnc (user)
		    Enable and start x11vnc for the local desktop (requires an existing X server).
		    Usually combined with jriver-mediaserver or jriver-mediacenter services.
		    --vncpass and --display are optional (see below).
		  jriver-xvnc (system)
		    Enable and start a new Xvnc session running JRiver Media Center.
		    --vncpass PASSWORD
		      Set the vnc password for x11vnc/Xvnc access. If no password is set, installJRMC
		      will either use existing password stored in \$HOME/.vnc/jrmc_passwd or else no password.
		    --display DISPLAY
		      Display to use for x11vnc/Xvnc (default: The current display (x11vnc) or the
		      current display incremented by 1 (Xvnc)).
		  jriver-createrepo (system)
		    Install hourly service to build latest MC RPM and run createrepo.

		ADVANCED OPTIONS
		  --container, -c CONTAINER (TODO: Under construction)
		    See CONTAINERS section below for a list of possible services to install.
		  --createrepo[=suse|fedora|centos|mandriva]
		    Build rpm, copy to webroot, and run createrepo. 
		    Use in conjunction with --build=TARGET for crossbuilding repos.
		    Optionally, specify a target distro for non-native repo (ex. --createrepo=fedora).
		  --createrepo-webroot PATH
		    Specify the webroot directory to install the repo (default: /var/www/jriver).
		  --webroot-user USER
		    Owner/user for createrepo output in the webroot (default: current user).
		  --createrepo-user USER
		    Backward-compatible alias for --webroot-user.
		  --sign
		    Sign the built RPM and repodata/repomd.xml (if --createrepo).
		  --sign-user USER
		    User account used to run rpmsign and gpg signing (default: current user).
		  --sign-key KEYID
		    GPG key ID, fingerprint, or UID used for --sign.
	EOF
}

# @description Parses user input and sets sensible defaults
# @arg $@ User input
parse_input() {
  debug "${FUNCNAME[0]}()" "$@"
  declare -gi BUILD_SWITCH REPO_INSTALL_SWITCH LOCAL_INSTALL_SWITCH \
    CONTAINER_INSTALL_SWITCH CREATEREPO_SWITCH SNAP_INSTALL_SWITCH \
    APPIMAGE_INSTALL_SWITCH COMPAT_SWITCH UNINSTALL_SWITCH YES_SWITCH \
    SIGN_SWITCH DEBUG=0
  declare -g MC_VERSION_USER MC_MVERSION_USER MC_RELEASE_USER MC_REPO_USER USER_ARCH MJR_FILE \
    BETAPASS SERVICE_TYPE VNCPASS USER_DISPLAY BUILD_TARGET CREATEREPO_TARGET \
    WEBROOT_USER SIGN_USER SIGN_KEY
  local long_opts short_opts input
  long_opts="install:,build::,outputdir:,mcversion:,arch:,mcrepo:,compat,"
  long_opts+="restorefile:,betapass:,"
  long_opts+="service-type:,service:,services:,"
  long_opts+="version,debug,verbose,help,uninstall,yes,auto,no-update,"
  long_opts+="createrepo::,createrepo-webroot:,webroot-user:,createrepo-user:,"
  long_opts+="sign,sign-user:,sign-key:,"
  long_opts+="vncpass:,display:,container:"
  short_opts="+i:b::s:c:uyvdh"

  if input=$(getopt -o $short_opts -l $long_opts -- "$@"); then
    eval set -- "$input"
    while true; do
      case $1 in
        --install|-i) shift;
          case $1 in
            local|rpm|deb) BUILD_SWITCH=1 LOCAL_INSTALL_SWITCH=1 ;;
            repo|remote|""|-*) REPO_INSTALL_SWITCH=1 ;;
            container) CONTAINER_INSTALL_SWITCH=1 ;;
            snap) SNAP_INSTALL_SWITCH=1 ;;
            appimage) APPIMAGE_INSTALL_SWITCH=1 ;;
            *) err "Invalid --install option passed"; exit 1 ;;
          esac
          ;;
        --build|-b) BUILD_SWITCH=1; shift; BUILD_TARGET="$1" ;;
        --outputdir) shift; OUTPUT_DIR="$1" ;;
        --mcversion) shift;
          if [[ $1 =~ ^([0-9]+)(\.[0-9]+\.[0-9]+)?(-([0-9]+))?$ ]]; then
            # Major version is required
            MC_MVERSION_USER="${BASH_REMATCH[1]}"
            # Set default release to 1 if not provided
            MC_RELEASE_USER="${BASH_REMATCH[4]:-1}"
            # If we get the full version, use it
            [[ -n ${BASH_REMATCH[2]} ]] && MC_VERSION_USER="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"

            # Set major version defaults
            case "$MC_MVERSION_USER" in
              35) MC_REPO_HARDCODE="bookworm" BOARD_ID="92.0" ;; # fallback to hardcoded version if full version not provided
              34) MC_VERSION_USER="${MC_VERSION_USER:-34.0.75}" MC_REPO_HARDCODE="bookworm" BOARD_ID="89.0" ;;
              33) MC_VERSION_USER="${MC_VERSION_USER:-33.0.72}" MC_REPO_HARDCODE="bullseye" BOARD_ID="86.0" ;;
              32) MC_VERSION_USER="${MC_VERSION_USER:-32.0.58}" MC_REPO_HARDCODE="bullseye" BOARD_ID="83.0" ;;
              31) MC_VERSION_USER="${MC_VERSION_USER:-31.0.83}" MC_REPO_HARDCODE="bullseye" BOARD_ID="80.0" ;;
              30) MC_VERSION_USER="${MC_VERSION_USER:-30.0.96}" MC_REPO_HARDCODE="buster" BOARD_ID="76.0" ;;
              29) MC_VERSION_USER="${MC_VERSION_USER:-29.0.91}" MC_REPO_HARDCODE="buster" BOARD_ID="74.0" ;;
              28) MC_VERSION_USER="${MC_VERSION_USER:-28.0.110}" MC_REPO_HARDCODE="buster" BOARD_ID="71.0" ;;
              27) MC_VERSION_USER="${MC_VERSION_USER:-27.0.88}"  MC_REPO_HARDCODE="buster" BOARD_ID="67.0" ;;
              26) MC_VERSION_USER="${MC_VERSION_USER:-26.0.107}" MC_REPO_HARDCODE="jessie" BOARD_ID="64.0" ;;
              25) MC_VERSION_USER="${MC_VERSION_USER:-25.0.114}" MC_REPO_HARDCODE="jessie" BOARD_ID="62.0" ;;
              24) MC_VERSION_USER="${MC_VERSION_USER:-24.0.78}" MC_REPO_HARDCODE="jessie" BOARD_ID="58.0" ;;
              23) MC_VERSION_USER="${MC_VERSION_USER:-23.0.104}" MC_REPO_HARDCODE="jessie" BOARD_ID="54.0" ;;
              22) MC_VERSION_USER="${MC_VERSION_USER:-22.0.102}" MC_REPO_HARDCODE="jessie" BOARD_ID="51.0" ;;
              21) MC_VERSION_USER="${MC_VERSION_USER:-21.0.90}" MC_REPO_HARDCODE="jessie" BOARD_ID="44.0" ;;
              20) MC_VERSION_USER="${MC_VERSION_USER:-20.0.131}" MC_REPO_HARDCODE="jessie" BOARD_ID="35.0" ;;
              *) 
                # Warn for future major beta versions
                if [[ $MC_MVERSION_USER -gt ${MC_VERSION_HARDCODE%%.*} ]] && [[ -z $MC_VERSION_USER ]]; then
                  echo "For future major versions, supply full version (and --betapass if necessary)."
                  err "Bad --mcversion"; print_help; exit 1
                fi
                ;;
            esac
          else
            err "Bad --mcversion"; print_help; exit 1
          fi
          ;;
        --arch) shift; USER_ARCH="$1" ;;
        --mcrepo) shift; MC_REPO_USER="$1" ;;
        --restorefile) shift; MJR_FILE="$1"; [[ -f $MJR_FILE ]] || err "Specified license $MJR_FILE missing." ;;
        --betapass) shift; BETAPASS="$1" ;;
        --service-type) shift; SERVICE_TYPE="$1" ;;
        --service|-s|--services) shift; SERVICES+=("$1") ;;
        --createrepo) shift; CREATEREPO_TARGET="$1"; BUILD_TARGET="$1"
          BUILD_SWITCH=1; CREATEREPO_SWITCH=1 ;;
        --createrepo-webroot) shift; CREATEREPO_WEBROOT="$1" ;;
        --webroot-user|--createrepo-user) shift; WEBROOT_USER="$1" ;;
        --sign) SIGN_SWITCH=1 ;;
        --sign-user) shift; SIGN_USER="$1" ;;
        --sign-key) shift; SIGN_KEY="$1" ;;
        --vncpass) shift; VNCPASS="$1" ;;
        --display) shift; USER_DISPLAY="$1" ;;
        --compat) COMPAT_SWITCH=1; BUILD_SWITCH=1 ;;
        --no-update) SELF_UPDATE_SWITCH=0 ;;
        --container|-c) shift; CONTAINERS+=("$1") ;;
        --yes|-y|--auto) YES_SWITCH=1 ;;
        --version|-v) echo "Version: $SCRIPT_VERSION"; exit 0 ;;
        --debug|-d|--verbose) DEBUG=1 ;;
        --help|-h) print_help; exit 0 ;;
        --uninstall|-u) UNINSTALL_SWITCH=1 ;;
        --) shift; break ;;
      esac
      shift
    done
  else
    err "Incorrect option provided, see installJRMC --help"; exit 1
  fi

  # Default to --install=repo (can be overridden later by OS defaults)
  if ! ((UNINSTALL_SWITCH || BUILD_SWITCH || CREATEREPO_SWITCH || LOCAL_INSTALL_SWITCH
  || CONTAINER_INSTALL_SWITCH || SNAP_INSTALL_SWITCH || APPIMAGE_INSTALL_SWITCH)) &&
  [[ ${#SERVICES[@]} -eq 0 && ${#CONTAINERS[@]} -eq 0 ]]; then
    debug "Defaulting to --install=repo"
    REPO_INSTALL_SWITCH=1
  fi

  if [[ -n $BETAPASS ]] && ((REPO_INSTALL_SWITCH)); then
    echo "Warning: not all repositories have beta channels"
    echo "If the MC package is unavailable, try using --mcrepo to select another repository"
  fi

  # If jriver-createrepo is being installed as a service, treat --createrepo
  # as service configuration only and defer build/repo execution to the timer.
  if [[ " ${SERVICES[*]} " =~ [[:space:]]jriver-createrepo[[:space:]] ]] && ((CREATEREPO_SWITCH)); then
    debug "Deferring --createrepo execution while configuring jriver-createrepo service"
    BUILD_SWITCH=0
    CREATEREPO_SWITCH=0
  fi
}

# @description Perform OS detection and generate OS-specific functions
# @see parse_input
init() {
  debug "${FUNCNAME[0]}()" "$@"
  declare -g USER
  declare -g SCRIPT_PATH; SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
  declare -g SCRIPT_DIR; SCRIPT_DIR=$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")
  declare -gi SCRIPT_IS_PIPED=0

  # Detect if script is being piped (SCRIPT_PATH won't be a regular file)
  [[ ! -f $SCRIPT_PATH ]] && SCRIPT_IS_PIPED=1
  
  declare -g OUTPUT_DIR="$SCRIPT_DIR/output"
  declare -g CREATEREPO_WEBROOT="/var/www/jriver"
  declare -g WEBROOT_USER # can be root
  declare -g SIGN_USER
  declare -g ID VERSION_ID UBUNTU_CODENAME VERSION_CODENAME ARCH MC_ARCH NAME
  declare -g MC_MVERSION MC_RELEASE MC_PKG MC_RPM MC_ROOT
  declare -ga PKG_INSTALL PKG_REMOVE PKG_UPDATE PKG_QUERY
  declare -ga SERVICES CONTAINERS

  parse_input "$@"

  # Try to save users from themselves
  if [[ $EUID -eq 0 ]]; then
    echo "Warning: running as root"
    ask_ok "Continue as root user (not recommended)?" || exit 1
  elif [[ -n $SUDO_USER ]]; then
    err "Sudo detected, installJRMC should not be run with sudo but attempting to continue"
    ask_ok "Continue as user $SUDO_USER (unsupported and may result in permission issues)?" || exit 1
    USER="${SUDO_USER:-$USER}"
  fi

  # Default webroot/signing contexts to the account currently running installJRMC.
  WEBROOT_USER="${WEBROOT_USER:-$USER}"
  SIGN_USER="${SIGN_USER:-$(id -un)}"

  # Run the self-updater if enabled
  ((SELF_UPDATE_SWITCH)) && ((! SCRIPT_IS_PIPED)) && update "$@"

  # Check that the .jriver directory is owned by the user
  ((YES_SWITCH)) || fix_permissions "$HOME/.jriver" "$USER"

  # Get host information and immediately fail if required vars are unavailable
  [[ -f /etc/os-release ]] && source /etc/os-release

  # Detect host architecture and translate to MC convention
  if ARCH=$(uname -m); then
    case $ARCH in
      x86_64) MC_ARCH="amd64" ;;
      aarch64) MC_ARCH="arm64" ;;
      *) MC_ARCH="$ARCH" ;;
    esac
  else
    ARCH="x86_64"
    MC_ARCH="amd64"
    err "Failed to detect host arch, using default: $ARCH"
  fi

  echo "Host: $ID $VERSION_ID $ARCH"

  # Parse user-provided architecture, allow either convention
  if [[ -n $USER_ARCH ]]; then
    case $USER_ARCH in
      x86_64|amd64) ARCH="x86_64"; MC_ARCH="amd64" ;;
      aarch64|arm64) ARCH="aarch64"; MC_ARCH="arm64" ;;
      *) ARCH="$USER_ARCH" ;;
    esac
  fi

  # Normalize ID and set OS defaults
  case $ID in
    debian|fedora|centos) ;;
    rhel|almalinux) ID="centos" ;;
    linuxmint|neon|zorin|*ubuntu*) ID="ubuntu" ;;
    raspbian) ID="debian" ;;
    *mandriva*) ID="mandriva"
      if ((REPO_INSTALL_SWITCH)); then
        debug "Automatically using --install=local for Mandriva."
        REPO_INSTALL_SWITCH=0
        BUILD_SWITCH=1
        LOCAL_INSTALL_SWITCH=1
      fi ;;
    manjaro|arch|cachyos) ID="arch"
      if ((REPO_INSTALL_SWITCH)); then
        debug "Automatically using --install=local for Arch."
        REPO_INSTALL_SWITCH=0
        BUILD_SWITCH=1
        LOCAL_INSTALL_SWITCH=1
      fi ;;
    *suse*) ID="suse"
      if ((REPO_INSTALL_SWITCH)); then
        debug "Automatically using --install=local for SUSE."
        REPO_INSTALL_SWITCH=0
        BUILD_SWITCH=1
        LOCAL_INSTALL_SWITCH=1
      fi ;;
    *) err "Auto-detecting OS, this is unreliable and --compat may be required."
      for cmd in dnf yum apt-get pacman; do
        if command -v "$cmd" &>/dev/null; then
          case "$cmd" in
            dnf) ID="fedora" ;;
            yum) ID="centos"; COMPAT_SWITCH=1 ;;
            apt-get) ID="ubuntu" ;;
            pacman) ID="arch" ;;
          esac
          break
        fi
      done

      if [[ -z $ID ]]; then
        err "OS detection failed!"
        if ask_ok "Continue with manual installation?"; then
          debug "Automatically using --install=local for unknown distro."
          ID="unknown"
          REPO_INSTALL_SWITCH=0
          BUILD_SWITCH=1
          LOCAL_INSTALL_SWITCH=1
        else
          exit 1
        fi
      fi ;;
  esac

  # Set distro-specific package manager commands for normalized OS
  case $ID in
    fedora|centos|mandriva)
      local rpm_mgr
      rpm_mgr=$(command -v dnf &>/dev/null && echo "dnf" || echo "yum")
      PKG_INSTALL=(sudo "$rpm_mgr" install --assumeyes)
      PKG_REMOVE=(sudo "$rpm_mgr" remove --assumeyes)
      PKG_UPDATE=(sudo "$rpm_mgr" makecache --assumeyes)
      PKG_QUERY=(rpm -q)
      ;;
    debian|ubuntu)
      PKG_INSTALL=(sudo apt-get install --fix-broken --install-recommends --assume-yes)
      PKG_REMOVE=(sudo apt-get remove --auto-remove --assume-yes)
      PKG_UPDATE=(sudo apt-get update --assume-yes)
      PKG_QUERY=(dpkg -s)
      ;;
    suse)
      PKG_INSTALL=(sudo zypper --gpg-auto-import-keys --non-interactive install --force --force-resolution --replacefiles --no-confirm)
      PKG_REMOVE=(sudo zypper --non-interactive remove --clean-deps)
      PKG_UPDATE=(sudo zypper --non-interactive refresh jriver)
      PKG_QUERY=(rpm --query)
      ;;
    arch)
      PKG_INSTALL=(sudo pacman --sync --refresh --noconfirm)
      PKG_REMOVE=(sudo pacman --remove --recursive --noconfirm)
      PKG_UPDATE=(sudo pacman --sync --refresh --refresh)
      PKG_QUERY=(sudo pacman --query --search)
      ;;
    unknown)
      PKG_INSTALL=(:)
      PKG_REMOVE=(:)
      PKG_UPDATE=(:)
      PKG_QUERY=(:)
      ;;
  esac

  # Set default targets
  BUILD_TARGET="${BUILD_TARGET:-$ID}"
  CREATEREPO_TARGET="${CREATEREPO_TARGET:-$ID}"
  MC_REPO="${MC_REPO_USER:-${UBUNTU_CODENAME:-${VERSION_CODENAME:-$MC_REPO_HARDCODE}}}" # user>host>hardcoded

  echo "MC source -> target: $MC_REPO $MC_ARCH -> $BUILD_TARGET $ARCH"
  set_mc_version
  if ((REPO_INSTALL_SWITCH || UNINSTALL_SWITCH)) && [[ -z $MC_VERSION_USER ]]; then
    echo "Selected latest MC version from the $MC_REPO repo (via $MC_VERSION_SOURCE)"
  else
    echo "Selected MC version $MC_VERSION from the $MC_REPO repo (via $MC_VERSION_SOURCE)"
  fi
  
  # Set additional MC version variables
  MC_RELEASE="${MC_RELEASE_USER:-1}"
  MC_MVERSION="${MC_MVERSION_USER:-${MC_VERSION%%.*}}"
  MC_PKG="mediacenter$MC_MVERSION"
  MC_RPM="$OUTPUT_DIR/RPMS/$ARCH/mediacenter$MC_MVERSION-$MC_VERSION-$MC_RELEASE.$ARCH.rpm"
  MC_ROOT="/usr/lib/jriver/Media Center $MC_MVERSION"

  # Generate explicit package name
  if [[ -n $MC_VERSION_USER ]]; then
    # Append explicit package version when user provides --mcversion
    case $ID in
      fedora|centos|suse|mandriva) MC_PKG+="-$MC_VERSION" ;;
      debian|ubuntu) MC_PKG+="=$MC_VERSION" ;;
    esac
  fi
}

# @description Determines the latest JRiver MC version using several methods
set_mc_version() {
  debug "${FUNCNAME[0]}()"
  declare -g MC_VERSION MC_VERSION_SOURCE

  if [[ -n $MC_VERSION_USER ]]; then
    MC_VERSION="$MC_VERSION_USER"
    MC_VERSION_SOURCE="user input"
    return 0  
  fi

  # Package manager will handle updates in other instances
  if ! ((BUILD_SWITCH || LOCAL_INSTALL_SWITCH || CREATEREPO_SWITCH)); then
    MC_VERSION="$MC_VERSION_HARDCODE"
    if ((REPO_INSTALL_SWITCH || UNINSTALL_SWITCH)); then
      MC_VERSION_SOURCE="package manager"
    else
      MC_VERSION_SOURCE="hardcoded"
    fi
    return 0
  fi

  # Determine latest version
  # Containerized package manager
  if create_mc_apt_container &&
  MC_VERSION=$(buildah run "$CNT" -- apt-cache policy "mediacenter${MC_MVERSION_USER:-${MC_VERSION_HARDCODE%%.*}}" | awk '/Candidate:/ {sub(/-.*/, "", $2); print $2}' | sort -V | tail -n1) &&
  buildah rm "$CNT" &>/dev/null &&
  [[ $MC_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    MC_VERSION_SOURCE="containerized package manager"
  # Fallback to webscrape
  elif MC_VERSION=$(download "https://yabb.jriver.com/interact/index.php/board,$BOARD_ID.html" "-" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1) &&
  [[ $MC_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    MC_VERSION_SOURCE="webscrape"
  # Fallback to hardcoded value
  else
    MC_VERSION="$MC_VERSION_HARDCODE"
    MC_VERSION_SOURCE="hardcoded"
  fi
}

# @description Installs a package using the system package manager
# @arg $1 array One or more package names
# @option --no-install-check Do not check if package is already installed
# @option --no-gpg-check Disable GPG checks for RPM based distros
# @option --allow-downgrades Useful for installing specific MC versions
# @option --silent | -s Do not print errors (useful for optional packages)
install_package() {
  debug "${FUNCNAME[0]}()" "$@"
  local -a pkg_array install_flags
  local -A pkg_aliases
  local input pkg _pkg
  local -i no_install_check=0 allow_downgrades=0 silent=0 refresh=0 no_gpg_check=0 reinstall=0
  local long_opts="no-install-check,allow-downgrades,no-gpg-check,refresh,reinstall,silent"
  local -a pkg_install=("${PKG_INSTALL[@]}")

  input=$(getopt -o +s -l "$long_opts" -- "$@") || { err "Incorrect options provided"; exit 1; }
  eval set -- "$input"
  
  while true; do
    case $1 in
      --no-install-check) no_install_check=1 ;;
      --allow-downgrades) allow_downgrades=1 ;;
      --no-gpg-check) no_gpg_check=1 ;;
      --refresh) refresh=1 ;;
      --reinstall) reinstall=1 ;;
      --silent|-s) silent=1 ;;
      --) shift; break ;;
    esac
    shift
  done

  # Distribution-specific package aliases
  case $ID in
    debian|ubuntu) pkg_aliases=(
      [rpm-build]="rpm"
      [createrepo_c]="createrepo"
      [tigervnc-server]="tigervnc-standalone-server"
    ) ;;
    suse) pkg_aliases=(
      [buildah]="buildah fuse-overlayfs"
    ) ;;
    mandriva) pkg_aliases=(
      [dpkg]="dpkg gnutar"
    ) ;;
  esac

  # Filter out already installed packages to create pkg_array
  for pkg in "$@"; do
    # Use alias if present, otherwise just pkg itself
    pkg_names=("$pkg")
    if [[ -v pkg_aliases[$pkg] ]]; then
      debug "Aliasing $pkg to ${pkg_aliases[$pkg]}"
      IFS=' ' read -ra pkg_names <<< "${pkg_aliases[$pkg]}"
    fi
    for p in "${pkg_names[@]}"; do
      if (( no_install_check )) ||
      ! { command -v "$p" &>/dev/null || "${PKG_QUERY[@]}" "$p" &>/dev/null; }; then
        pkg_array+=("$p")
      else
        debug "$p is already installed, skipping installation"
      fi
    done
  done

  # Add OS install flags to package manager command
  case $ID in
    debian|ubuntu)
      ((allow_downgrades)) && install_flags+=(--allow-downgrades)
      ((reinstall)) && install_flags+=(--reinstall) ;;
    fedora|centos|mandriva)
      ((allow_downgrades)) && install_flags+=(--allowerasing)
      ((no_gpg_check)) && install_flags+=(--nogpgcheck)
      ((refresh)) && install_flags+=(--refresh)
      ;;
    suse)
      ((no_gpg_check)) && install_flags+=(--allow-unsigned-rpm) ;;
  esac

  ((silent)) && install_flags+=(--quiet)

  # Install packages
  if [[ ${#pkg_array[@]} -gt 0 ]]; then
    if ! "${pkg_install[@]}" "${install_flags[@]}" "${pkg_array[@]}"; then     
      ((silent)) || err "Failed to install ${pkg_array[*]}"
      return 1
    fi
  fi
  return 0
}

# @description Installs host-specific external repos
install_external_repos() {
  debug "${FUNCNAME[0]}()"

  case $ID in
    ubuntu)
      if ! grep -E '^deb|^Components' /etc/apt/sources.list /etc/apt/sources.list.d/* | grep -q universe; then
        echo "Adding universe repository"
        if ! execute sudo add-apt-repository -y universe; then
          err "Adding universe repository failed"
        fi
      fi
      ;;
    centos)
      if ! command -v dpkg &>/dev/null; then
        echo "Adding EPEL repository"
        if ! install_package epel-release; then
          # If epel-release is not available, install it manually
          install_package --no-install-check \
            "https://dl.fedoraproject.org/pub/epel/epel-release-latest-${VERSION_ID%%.*}.noarch.rpm"
        fi
      fi
      if ! "${PKG_QUERY[@]}" rpmfusion-free-release &>/dev/null; then
        echo "Installing the RPMFusion repository"
        install_package --no-install-check \
          "https://download1.rpmfusion.org/free/el/rpmfusion-free-release-${VERSION_ID%%.*}.noarch.rpm"
      fi
      ;;
    fedora)
      if ! "${PKG_QUERY[@]}" rpmfusion-free-release &>/dev/null; then
        echo "Installing the RPMFusion repository"
        install_package --no-install-check \
          "https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$VERSION_ID.noarch.rpm"
      fi
      # Install freeworld mesa packages for better hardware acceleration
      for pkg in mesa-va-drivers mesa-vdpau-drivers mesa-vulkan-drivers; do
        freeworld_pkg="${pkg}-freeworld"
        if ! "${PKG_QUERY[@]}" "$freeworld_pkg" &>/dev/null; then
          if "${PKG_QUERY[@]}" "$pkg" &>/dev/null; then
            if ! execute sudo dnf swap -y "$pkg" "$freeworld_pkg"; then
              err "Package swap failed for $pkg!"
            fi
          else
            "${PKG_INSTALL[@]}" "$freeworld_pkg"
          fi
        fi
      done
      ;;
    suse) : # TODO may be needed if X11_XOrg is made unavailable in default repos
      # if ! zypper repos | grep -q "X11_XOrg"; then
      #   echo "Installing the X11 repository"
      #   execute sudo zypper  --non-interactive --quiet addrepo \
      #     "https://download.opensuse.org/repositories/X11:/XOrg/${NAME// /_}/X11:XOrg.repo"
      #   execute sudo zypper  --non-interactive --quiet refresh
      # fi
      ;;
    mandriva)
      local branch
      branch=$(grep ^PRETTY_NAME= /etc/os-release | tr -d '"' | rev | cut -d' ' -f1 | rev | tr '[:upper:]' '[:lower:]')
      execute sudo dnf config-manager --set-enabled "${branch}-${ARCH}-extra"
      ;;
  esac
}

# @description Installs host-specific temporary legacy repo for missing dependencies
add_legacy_repo() {
  debug "${FUNCNAME[0]}()"
  local repo_name repo_uri repo_suite repo_key temp_repo_file

  case $ID in
    ubuntu)
      if [[ $UBUNTU_CODENAME =~ ^[n-z] ]]; then # noble and later
        echo "Temporarily adding jammy repository for libwebkit2gtk-4.0-37, etc."
        repo_name="ubuntu-jammy-temp"
        repo_uri="https://archive.ubuntu.com/ubuntu"
        repo_suite="jammy"
        repo_key="/usr/share/keyrings/ubuntu-archive-keyring.gpg"
      fi
      ;;
    debian)
      if [[ ${VERSION_ID%%.*} -ge 13 ]]; then
        echo "Temporarily adding bookworm repository for libwebkit2gtk-4.0-37, etc."
        repo_name="debian-bookworm-temp"
        repo_uri="https://deb.debian.org/debian"
        repo_suite="bookworm"
        repo_key="/usr/share/keyrings/debian-archive-keyring.gpg"
      fi
      ;;
  esac

  if [[ -n $repo_name ]]; then
    echo "Adding temporary repository: $repo_name"
    temp_repo_file="/etc/apt/sources.list.d/$repo_name.sources"
    sudo tee "$temp_repo_file" &>/dev/null <<-EOF
			Types: deb
			URIs: $repo_uri
			Suites: $repo_suite
			Components: main
			Architectures: $MC_ARCH
			Signed-By: $repo_key
		EOF
    declare -g LEGACY_REPO_FILE="$temp_repo_file"
  fi
}

# @description Removes temporary legacy repository if present
remove_legacy_repo() {
  debug "${FUNCNAME[0]}()"
  [[ -n $LEGACY_REPO_FILE ]] && execute sudo rm -f "$LEGACY_REPO_FILE"
}

# @description Acquires the source DEB package from JRiver
acquire_deb() {
  debug "${FUNCNAME[0]}()"
  declare -g MC_DEB MC_SOURCE
  local fname

  [[ -d $OUTPUT_DIR/SOURCES ]] || execute mkdir -p "$OUTPUT_DIR/SOURCES"

  # Usually JRiver excludes the release number from the filename
  # but in some cases (test builds) it may be included
  if [[ $MC_RELEASE -gt 1 ]]; then
    fname="MediaCenter-$MC_VERSION-$MC_RELEASE-$MC_ARCH.deb"
  else
    fname="MediaCenter-$MC_VERSION-$MC_ARCH.deb"
  fi

  MC_DEB="$OUTPUT_DIR/SOURCES/$fname"
  MC_SOURCE="https://files.jriver-cdn.com/mediacenter/channels/v$MC_MVERSION/latest/$fname"

  # If deb file already exists and is >30MB, skip download
  if [[ -f $MC_DEB ]]; then
    if [[ $(stat -c%s "$MC_DEB") -lt 30000000 ]]; then
      echo "Removing existing DEB under 30MB: $MC_DEB"
      execute rm -f "$MC_DEB"
    else
      echo "Using existing DEB: $MC_DEB"
      return 0
    fi
  fi

  # Download the deb file using containerized package manager
  # shellcheck disable=SC2016
  if ! {
    # Download to /tmp to silence _apt permission warnings
    create_mc_apt_container "cd /tmp && apt-get download --allow-unauthenticated mediacenter$MC_MVERSION -qq" &&
    env CNT="$CNT" MC_DEB="$MC_DEB" buildah unshare -- bash -eu -o pipefail -c '
      mnt="$(buildah mount "$CNT")"
      deb="$(find "$mnt/tmp" -maxdepth 1 -type f -name "*.deb" | head -n1)"
      [[ -n "$deb" ]] && cp -f "$deb" "$MC_DEB"
      buildah umount "$CNT"' && 
    [[ -f $MC_DEB ]] &&
    buildah rm "$CNT" &>/dev/null;
  }; then
    debug "Failed to download DEB using containerized package manager"
    echo "Using legacy download method"
    # Define the repository search order
    local -a repos
    [[ -n $BETAPASS ]] && repos=("https://files.jriver-cdn.com/mediacenter/channels/v$MC_MVERSION/beta/$BETAPASS/$fname")
    repos+=(
      "https://files.jriver-cdn.com/mediacenter/channels/v$MC_MVERSION/latest/$fname"
      "https://files.jriver-cdn.com/mediacenter/test/$fname")

    # Loop through the repositories and attempt to download
    for repo in "${repos[@]}"; do
      echo -n "$repo --> "
      if download "$repo" "$MC_DEB"; then
        echo "Found!"
        MC_SOURCE="$repo"
        break
      fi
      echo "Not found"
    done
  fi

  # Return if the download was successful
  [[ -f $MC_DEB ]]
}

# @description Translates upstream DEB dependencies for several distros
translate_packages() {
  debug "${FUNCNAME[0]}()" "$*"
  local deb_file="$1"
  # shellcheck disable=SC2178
  declare -n requires_arr="$2" recommends_arr="$3"
  local -i i

  # Load deb dependencies into array
  IFS=',' read -ra requires_arr <<< "$(dpkg-deb -f "$deb_file" Depends)"
  IFS=',' read -ra recommends_arr <<< "$(dpkg-deb -f "$deb_file" Recommends)"

  # Clean up formatting
  requires_arr=("${requires_arr[@]%%|*}")
  requires_arr=("${requires_arr[@]/?:/}")
  requires_arr=("${requires_arr[@]# }")
  requires_arr=("${requires_arr[@]% }")
  requires_arr=("${requires_arr[@]//\(/}")
  requires_arr=("${requires_arr[@]//)/}")
  recommends_arr=("${recommends_arr[@]%%|*}")
  recommends_arr=("${recommends_arr[@]/?:/}")
  recommends_arr=("${recommends_arr[@]# }")
  recommends_arr=("${recommends_arr[@]% }")
  recommends_arr=("${recommends_arr[@]//\(/}")
  recommends_arr=("${recommends_arr[@]//)/}")

  # Translate package names
  case $BUILD_TARGET in
    fedora|centos)
      requires_arr=("${requires_arr[@]/libc6/glibc}")
      requires_arr=("${requires_arr[@]/libasound2/alsa-lib}")
      requires_arr=("${requires_arr[@]/libuuid1/libuuid}")
      requires_arr=("${requires_arr[@]/libx11-6/libX11}")
      requires_arr=("${requires_arr[@]/libxext6/libXext}")
      requires_arr=("${requires_arr[@]/libxcb1*/libxcb}") # TODO Remove minimum version for MC31 (*)
      requires_arr=("${requires_arr[@]/libxau6/libXau}")
      requires_arr=("${requires_arr[@]/libxdmcp6/libXdmcp}")
      requires_arr=("${requires_arr[@]/libstdc++6/libstdc++}")
      requires_arr=("${requires_arr[@]/libgtk-3-0/gtk3}")
      requires_arr=("${requires_arr[@]/libegl1/mesa-libEGL}")
      requires_arr=("${requires_arr[@]/libgl1/mesa-libGL}")
      requires_arr=("${requires_arr[@]/libgles2/libglvnd-gles}")
      requires_arr=("${requires_arr[@]/libgbm1/mesa-libgbm}")
      requires_arr=("${requires_arr[@]/libegl-mesa0/mesa-libEGL}")
      requires_arr=("${requires_arr[@]/libvulkan1/vulkan-loader}")
      requires_arr=("${requires_arr[@]/libpango1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libpango-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libpangoft2-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libpangox-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libpangoxft-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libnss3/nss}")
      requires_arr=("${requires_arr[@]/libnspr4/nspr}")
      requires_arr=("${requires_arr[@]/libgomp1/libgomp}")
      requires_arr=("${requires_arr[@]/libfribidi0/fribidi}")
      requires_arr=("${requires_arr[@]/libfontconfig1/fontconfig}")
      requires_arr=("${requires_arr[@]/libfreetype6/freetype}")
      requires_arr=("${requires_arr[@]/libharfbuzz0b/harfbuzz}")
      requires_arr=("${requires_arr[@]/libva2/libva}")
      requires_arr=("${requires_arr[@]/libva-drm2/libva}")
      requires_arr=("${requires_arr[@]/libepoxy0/libepoxy}")
      requires_arr=("${requires_arr[@]/liblcms2-2/lcms2}")
      requires_arr=("${requires_arr[@]/libwebkit2gtk-4.0*/webkit2gtk4.0}")
      requires_arr=("${requires_arr[@]/libwebkit2gtk-4.1*/webkit2gtk4.1}")
      requires_arr=("${requires_arr[@]/libsdbus-c++1/sdbus-cpp}")
      requires_arr=("${requires_arr[@]/libdbus-1-3/dbus-libs}")
      requires_arr=("${requires_arr[@]/libxss1/libXScrnSaver}")
      recommends_arr=("${recommends_arr[@]/fdkaac/fdk-aac-free}")
      recommends_arr+=("mesa-va-drivers-freeworld|mesa-va-drivers")
      recommends_arr+=("mesa-vulkan-drivers-freeworld|mesa-vulkan-drivers")
      recommends_arr+=("mesa-vdpau-driver-freeworld|mesa-vdpau-driver")
      ;;
    suse)
      requires_arr=("${requires_arr[@]/python*/python3}")
      requires_arr=("${requires_arr[@]/libc6/glibc}")
      requires_arr=("${requires_arr[@]/libasound2/alsa-lib}")
      requires_arr=("${requires_arr[@]/libx11-6/libX11-6}")
      requires_arr=("${requires_arr[@]/libxext6/libXext6}")
      requires_arr=("${requires_arr[@]/libxdmcp6/libXdmcp6}")
      requires_arr=("${requires_arr[@]/libgtk-3-0/gtk3}")
      requires_arr=("${requires_arr[@]/libgl1/Mesa-libGL1}")
      requires_arr=("${requires_arr[@]/libpango-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libpangoft2-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libpangox-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libpangoxft-1.0-0/pango}")
      requires_arr=("${requires_arr[@]/libnss3/mozilla-nss}")
      requires_arr=("${requires_arr[@]/libnspr4/mozilla-nspr}")
      requires_arr=("${requires_arr[@]/libfribidi0/fribidi}")
      requires_arr=("${requires_arr[@]/libfontconfig1/fontconfig}")
      requires_arr=("${requires_arr[@]/libharfbuzz0b/libharfbuzz0}")
      requires_arr=("${requires_arr[@]/libwebkit2gtk-4.0*/libwebkit2gtk-4_0-37}")
      requires_arr=("${requires_arr[@]/libwebkit2gtk-4.1*/libwebkit2gtk-4_1-0}")
      requires_arr=("${requires_arr[@]/libxss1/libXss1}")
      for i in "${!requires_arr[@]}"; do
        [[ ${requires_arr[$i]} == "mesa-vulkan-drivers" ]] && unset -v 'requires_arr[i]'
        [[ ${requires_arr[$i]} == "libsdbus-c++1" ]] && unset -v 'requires_arr[i]'
      done
      recommends_arr+=(libvulkan1)
      recommends_arr+=(libvulkan_intel)
      recommends_arr+=(libvulkan_radeon)
      recommends_arr+=(libvulkan_nouveau)
      ;;
    mandriva)
      requires_arr=("${requires_arr[@]/libc6/glibc}")
      requires_arr=("${requires_arr[@]/libasound2/lib64asound2}")
      requires_arr=("${requires_arr[@]/libuuid1/lib64uuid1}")
      requires_arr=("${requires_arr[@]/libx11-6/lib64x11_6}")
      requires_arr=("${requires_arr[@]/libxext6/lib64xext6}")
      requires_arr=("${requires_arr[@]/libxcb1/lib64xcb1}")
      requires_arr=("${requires_arr[@]/libxdmcp6/lib64xdmcp6}")
      requires_arr=("${requires_arr[@]/libstdc++6/lib64stdc++6}")
      requires_arr=("${requires_arr[@]/libgtk-3-0/lib64gtk3_0}")
      requires_arr=("${requires_arr[@]/libgl1/lib64GL1}")
      requires_arr=("${requires_arr[@]/libgles2/lib64GLESv2_2}")
      requires_arr=("${requires_arr[@]/libegl-mesa0/lib64EGL_mesa0}")
      requires_arr=("${requires_arr[@]/libpango1.0-0/lib64pango1.0_0}")
      requires_arr=("${requires_arr[@]/libpango-1.0-0/lib64pango1.0_0}")
      requires_arr=("${requires_arr[@]/libpangoft2-1.0-0/lib64pangoft2_1.0_0}")
      requires_arr=("${requires_arr[@]/libpango-cairo-1.0-0/lib64pangocairo1.0_0}")
      requires_arr=("${requires_arr[@]/libpangoxft-1.0-0/lib64pangoxft1.0_0}")
      requires_arr=("${requires_arr[@]/libnss3/lib64nss3}")
      requires_arr=("${requires_arr[@]/libnspr4/lib64nspr4}")
      requires_arr=("${requires_arr[@]/libgomp1/lib64gomp1}")
      requires_arr=("${requires_arr[@]/libfribidi0/lib64fribidi0}")
      requires_arr=("${requires_arr[@]/libfontconfig1/lib64fontconfig}")
      requires_arr=("${requires_arr[@]/libfreetype6/lib64freetype6}")
      requires_arr=("${requires_arr[@]/libharfbuzz0b/lib64harfbuzz}")
      requires_arr=("${requires_arr[@]/libgbm1/lib64gbm1}")
      requires_arr=("${requires_arr[@]/libva2/lib64va2}")
      requires_arr=("${requires_arr[@]/libva-drm2/lib64va-drm2}")
      requires_arr=("${requires_arr[@]/libvulkan1/lib64vulkan1}")
      requires_arr=("${requires_arr[@]/mesa-vulkan-drivers/lib64dri-drivers}")
      requires_arr=("${requires_arr[@]/vulkan-icd/vulkan-loader}")
      requires_arr=("${requires_arr[@]/libwebkit2gtk-4.1-0/lib64webkit2gtk4.1}")
      requires_arr=("${requires_arr[@]/libdbus-1-3/libdbus-1_3}")
      requires_arr=("${requires_arr[@]/libxss1/libxscrnsaver1}")
      recommends_arr=("${recommends_arr[@]/musepack-tools/mppenc}")
      for i in "${!recommends_arr[@]}"; do
        [[ ${recommends_arr[$i]} == "fdkaac" ]] && unset -v 'recommends_arr[i]'
      done
      ;;
    arch)
      # Set these manually for Arch since they are quite different
      requires_arr=('alsa-lib' 'ca-certificates' 'dbus' 'gtk3' 'gcc-libs' 'libx11' 'libxext'
        'libxcb' 'libxau' 'libxdmcp' 'libxss' 'util-linux' 'mesa-libgl' 'webkit2gtk-4.1')
      recommends_arr=('mesa-libgl' 'nvidia-libgl' 'nvidia-utils' 'vulkan-intel'
        'vulkan-radeon' 'vorbis-tools' 'musepack-tools')
      ;;
    *) echo "Skipping package translations for $ID" ;;
  esac
}

# @description Creates an RPM .spec file and builds the RPM from the source DEB using rpmbuild
build_rpm() {
  debug "${FUNCNAME[0]}()"
  # shellcheck disable=SC2178
  declare -n requires_arr="$1" recommends_arr="$2"
  local requires_str recommends_str 
  local i rpmbuild_cmd sign_cmd stub sign_output
  local -a build_prefix sign_prefix
  local spec_file="$OUTPUT_DIR/SPECS/mediacenter$MC_MVERSION-$MC_VERSION-$MC_RELEASE-$BUILD_TARGET-$ARCH.spec"

  # skip rebuilding the rpm if it already exists
  debug "Checking for existing MC RPM: $MC_RPM"
  if [[ -f $MC_RPM && -f $spec_file ]]; then
    echo "Skipping build step: .spec and ouput RPM already exist"
    debug "RPM .spec file: $spec_file"
    debug "RPM: $MC_RPM"
    echo "Remove either to force rebuild"
    return 0
  fi

  # Exclude MC stub executable <= MC31
  if [[ $MC_MVERSION -le 31 ]]; then
    stub=""
  else
    stub="%{_bindir}/mc$MC_MVERSION"
  fi

  # Convert array to newline delim'd string (for heredoc)
  printf -v requires_str "Requires: %s\n" "${requires_arr[@]}"
  printf -v recommends_str "Recommends: %s\n" "${recommends_arr[@]}"

  unset requires_arr recommends_arr

  # Strip last newline
  requires_str="${requires_str%?}" 
  recommends_str="${recommends_str%?}"

  if ((COMPAT_SWITCH)); then
    # Strip minimum versions
    requires_str=$(echo "$requires_str" | awk -F" " 'NF == 4 {print $1 " " $2} NF != 4 {print $0}')
  fi

  # Create spec file
  cat <<-EOF > "$spec_file"
		Name: mediacenter$MC_MVERSION
		Version: $MC_VERSION
		Release: $MC_RELEASE
		Summary: JRiver Media Center
		Group: Applications/Media
		License: LicenseRef-JRiver-Proprietary
		URL: https://www.jriver.com/
		Source0: $MC_SOURCE

		BuildArch: $ARCH

		%global _rpmfilename %%{ARCH}/%%{NAME}-%%{version}-%%{release}.%%{ARCH}.rpm
		%undefine _source_date_epoch_from_changelog
		%define __brp_strip /bin/true
		%define __brp_strip_comment_note /bin/true
		%define __brp_strip_static_archive /bin/true
		%define __brp_strip_lto /bin/true

		AutoReqProv: no

		$requires_str
		$recommends_str

		Provides: mediacenter$MC_MVERSION

		%define __provides_exclude_from ^%{_libdir}/jriver/.*/.*\\.so.*$

		%description
		Media Center is more than a world class player.

		%prep

		%build

		%install
		dpkg -x %{S:0} %{buildroot}

		%files
		%{_bindir}/mediacenter$MC_MVERSION
		$stub
		%{_libdir}/jriver
		%{_datadir}
		%exclude %{_datadir}/applications/media_center_packageinstaller_$MC_MVERSION.desktop
		/etc/security/limits.d/*
	EOF

  # Run rpmbuild
  echo "Building $MC_RPM, this may take some time"
  rpmbuild_cmd=(
    rpmbuild 
      --define="_topdir $OUTPUT_DIR" 
      --define="_libdir /usr/lib"
      --target="$ARCH"
      -bb
      "$spec_file"
  )

  # Build as signing user when running as root with a non-root SIGN_USER.
  # This keeps RPM ownership aligned with rpmsign and avoids permission mismatches.
  if [[ $(id -un) == "$SIGN_USER" ]]; then
    build_prefix=()
  else
    build_prefix=(sudo -H -u "$SIGN_USER")
    execute chown -R "$SIGN_USER:$SIGN_USER" "$OUTPUT_DIR"
  fi

  # Run rpmbuild and verify output RPM exists
  execute "${build_prefix[@]}" "${rpmbuild_cmd[@]}" && [[ -f $MC_RPM ]] || return 1

  # Optionally sign the built RPM with the configured key
  if ((SIGN_SWITCH)); then
    command -v rpmsign &>/dev/null || { err "rpmsign command missing (install rpm-sign/rpm-build)"; return 1; }
    command -v gpg &>/dev/null || { err "gpg command missing"; return 1; }

    if ! id "$SIGN_USER" &>/dev/null; then
      err "Signing user does not exist: $SIGN_USER"
      return 1
    fi

    if [[ $(id -un) == "$SIGN_USER" ]]; then
      sign_prefix=()
    else
      # Use target HOME so rpmsign reads the expected user keyring.
      sign_prefix=(sudo -H -u "$SIGN_USER")
    fi

    if [[ -n $SIGN_KEY ]] && ! "${sign_prefix[@]}" gpg --batch --list-secret-keys --with-colons "$SIGN_KEY" 2>/dev/null | grep -q '^sec'; then
      err "Signing key not found in $SIGN_USER keyring: $SIGN_KEY"
      err "Import the private key for $SIGN_USER or adjust --sign-user/--sign-key"
      return 1
    fi

    sign_cmd=(rpmsign --addsign)
    if [[ -n $SIGN_KEY ]]; then
      if rpmsign --help 2>&1 | grep -q -- '--key-id'; then
        sign_cmd+=(--key-id "$SIGN_KEY")
      else
        sign_cmd+=(--define "_gpg_name $SIGN_KEY")
      fi
    fi
    sign_cmd+=("$MC_RPM")
    echo "Signing RPM: $MC_RPM"
    debug "${sign_prefix[*]} ${sign_cmd[*]}"

    if ! sign_output=$("${sign_prefix[@]}" "${sign_cmd[@]}" 2>&1); then
      err "RPM signing failed"
      [[ -n $sign_output ]] && echo "$sign_output" >&2
      err "Hint: for non-interactive service runs, ensure $SIGN_USER can access an unlocked GPG key"
      return 1
    fi

    ((DEBUG)) && [[ -n $sign_output ]] && echo "$sign_output"
  fi

  return 0
}

# @description Creates the Arch PKGBUILD file for Media Center
build_pkgbuild() {
  debug "${FUNCNAME[0]}()"
  # shellcheck disable=SC2178
  declare -n requires_arr="$1" recommends_arr="$2"
  local pkgbuild_file="$OUTPUT_DIR/PKGBUILD/mediacenter.pkgbuild"

  [[ -d $OUTPUT_DIR/PKGBUILD ]] || execute mkdir -p "$OUTPUT_DIR/PKGBUILD"

  # Create PKGBUILD file
  cat <<-EOF > "$pkgbuild_file"
		pkgname=mediacenter$MC_MVERSION
		pkgver=$MC_VERSION
		pkgrel=$MC_RELEASE
		pkgdesc="JRiver Media Center"
		arch=("$ARCH")
		url="https://www.jriver.com/"
		license=("custom")
		depends=(${requires_arr[@]})
		optdepends=(${recommends_arr[@]})
		source=("$MC_SOURCE")

		package() {
			cd "\$srcdir"
			bsdtar xf data.tar.xz -C "\$pkgdir"
		}
	EOF
  unset requires_arr recommends_arr
}

# @description Installs Media Center via DEB package w/ optional compatability fixes
install_mc_deb() {
  debug "${FUNCNAME[0]}()" "$@"

  if ((COMPAT_SWITCH)); then
    local extract_dir; extract_dir="$(mktemp -d)"
    pushd "$extract_dir" &>/dev/null || return
    command -v ar &>/dev/null || { install_package binutils || return 1; }
    execute ar x "$MC_DEB"
    execute tar xJf "control.tar.xz"
    # Remove minimum version specifiers from control file
    execute sed -i 's/ ([^)]*)//g' control
    # Remove libwebkit2gtk and their fantastic package versioning strategy
    execute sed -E -i 's/,[[:space:]]*libwebkit2gtk[^,]*(,|\?)?//g' control

    # TODO workaround for legacy ZorinOS
    if [[ $ID == "ubuntu" && ${VERSION_ID%%.*} -le 16 ]] &&
    grep -q zorin /etc/os-release; then
      execute sed -i 's/libva2/libva1/g' control
    fi

    execute tar -cJf "control.tar.xz" "control" "postinst"
    declare -g MC_DEB="${MC_DEB/.deb/.compat.deb}"
    execute ar rcs "$MC_DEB" "debian-binary" "control.tar.xz" "data.tar.xz"
    popd &>/dev/null || return
    execute rm -rf "$extract_dir"
  fi

  # Add older repository for libwebkit2gtk-4.0-37, etc, on newer Debian/Ubuntu
  add_legacy_repo
  execute "${PKG_UPDATE[@]}" || { err "Package update failed!"; remove_legacy_repo; return 1; }

  # Copy the DEB to a temporary file so _apt can read it
  debug "Creating temporary deb file owned by _apt"
  local temp_deb
  temp_deb=$(mktemp --suffix=.deb)
  execute sudo cp "$MC_DEB" "$temp_deb"
  id _apt &>/dev/null && execute sudo chown _apt "$temp_deb"

  # Use --reinstall to make sure local package is installed over repo package
  if ! install_package \
    --no-install-check \
    --no-gpg-check \
    --allow-downgrades \
    --reinstall \
    "$temp_deb"; then
    err "Local MC DEB installation failed"
    remove_legacy_repo
    execute sudo rm -f "$temp_deb"
    if ask_ok "Remove source DEB and retry?"; then
      execute sudo rm -f "$MC_DEB"
      rerun "$@"
    fi
    return 1
  fi
  remove_legacy_repo
  execute sudo rm -f "$temp_deb"
  return 0
}

# @description Installs Media Center generically for unsupported OSes
install_mc_generic() {
  debug "${FUNCNAME[0]}()"
  local extract_dir
  local -a raw_files

  echo "Using generic installation method"

  extract_dir="$(mktemp -d)"
  pushd "$extract_dir" &>/dev/null || return
  execute ar x "$MC_DEB"
  execute tar xJf "control.tar.xz"
  echo "You must install the following dependencies manually:"
  grep -i "Depends:" control
  readarray -t raw_files < <(tar xJvf data.tar.xz)
  # Output to log file
  for f in "${raw_files[@]/#./}"; do
    echo "$f" >> "$SCRIPT_DIR/.uninstall"
  done
  # Manually install files
  for f in "${raw_files[@]}"; do
    execute sudo cp -a "$f" "${f/#./}"
  done
  popd &>/dev/null || return
  execute rm -rf "$extract_dir"
  return 0
}

# @description Installs MC via PKGBUILD
install_mc_arch() {
  debug "${FUNCNAME[0]}()"
  
  execute "${PKG_INSTALL[@]}" fakeroot # makepkg requires fakeroot

  pushd "$OUTPUT_DIR/PKGBUILD" &>/dev/null || return

  if ! execute makepkg \
    --install \
    --syncdeps \
    --clean \
    --cleanbuild \
    --skipinteg \
    --force \
    --noconfirm \
    -p mediacenter.pkgbuild; then
    err "makepkg failed"; exit 1
  fi
 
  popd &>/dev/null || return
}

# @description Copy the RPM to createrepo-webroot and run createrepo as the webroot-user
run_createrepo() {
  debug "${FUNCNAME[0]}()"
  local -a cr_opts gpg_cmd sign_prefix
  local repomd_xml repomd_asc pubkey_file

  install_package createrepo_c

  # Ensure WEBROOT_USER exists or offer to create it
  if ! id "$WEBROOT_USER" &>/dev/null; then
    err "Specified --webroot-user '$WEBROOT_USER' does not exist"
    if ask_ok "Create local user '$WEBROOT_USER'?"; then
      if ! execute sudo useradd "$WEBROOT_USER"; then
        err "Failed to create user '$WEBROOT_USER'"
        return 1
      fi
    else
      err "Cannot continue without a valid --webroot-user"
      return 1
    fi
  fi

  # Ensure the webroot exists
  if [[ ! -d $CREATEREPO_WEBROOT ]]; then
    if ! execute sudo -u "$WEBROOT_USER" mkdir -p "$CREATEREPO_WEBROOT"; then
      if ! (execute sudo mkdir -p "$CREATEREPO_WEBROOT" ||
      execute sudo chown -R "$WEBROOT_USER:$WEBROOT_USER" "$CREATEREPO_WEBROOT"); then
        err "Could not create the createrepo-webroot path!"
        err "Make sure that the webroot $CREATEREPO_WEBROOT is writable by user $WEBROOT_USER"
        err "Or change the repo ownership with --webroot-user"
        return 1
      fi
    fi
  fi

  # Copy built RPMs to webroot
  if ! execute sudo cp -nf "$MC_RPM" "$CREATEREPO_WEBROOT" ||
  ! execute sudo chown -R "$WEBROOT_USER:$WEBROOT_USER" "$CREATEREPO_WEBROOT"; then
    err "Could not copy $MC_RPM to $CREATEREPO_WEBROOT"
    return 1
  fi

  # Run createrepo
  cr_opts=(--update)
  # [[ -d "$CREATEREPO_WEBROOT/repodata" ]] && cr_opts+=(--update) # TODO temporarily disabled for legacy createrepo
  if ! execute sudo -u "$WEBROOT_USER" createrepo "${cr_opts[@]}" "$CREATEREPO_WEBROOT"; then
    if ! (execute sudo createrepo "${cr_opts[@]}" "$CREATEREPO_WEBROOT" && execute sudo chown -R "$WEBROOT_USER:$WEBROOT_USER" "$CREATEREPO_WEBROOT"); then
      err "createrepo failed"
      return 1
    fi
  fi

  # Optionally sign repodata so clients can use repo_gpgcheck=1
  if ((SIGN_SWITCH)); then
    command -v gpg &>/dev/null || { err "gpg command missing"; return 1; }
    repomd_xml="$CREATEREPO_WEBROOT/repodata/repomd.xml"
    repomd_asc="$repomd_xml.asc"
    [[ -f $repomd_xml ]] || { err "repomd.xml missing after createrepo"; return 1; }
    [[ -n $SIGN_KEY ]] || { err "--sign requires --sign-key for repodata signing"; return 1; }

    if [[ $(id -un) == "$SIGN_USER" ]]; then
      sign_prefix=()
    else
      sign_prefix=(sudo -H -u "$SIGN_USER")
    fi

    # Sign repo.md to a temp file first and then move to webroot
    local repomd_asc_tmp
    if ! repomd_asc_tmp=$("${sign_prefix[@]}" mktemp); then
      err "Failed to create temp file for signature"
      return 1
    fi

    gpg_cmd=(gpg --batch --yes --pinentry-mode loopback --default-key "$SIGN_KEY" --armor --detach-sign --output "$repomd_asc_tmp")
    ((DEBUG)) && gpg_cmd+=(--verbose)
    gpg_cmd+=("$repomd_xml")

    echo "Signing repodata: $repomd_xml"
    if ! execute "${sign_prefix[@]}" "${gpg_cmd[@]}"; then
      rm -f "$repomd_asc_tmp"
      err "Repodata signing failed"
      return 1
    fi
    execute sudo install -m 0644 "$repomd_asc_tmp" "$repomd_asc"
    execute sudo chown "$WEBROOT_USER:$WEBROOT_USER" "$repomd_asc"
    rm -f "$repomd_asc_tmp"

    # Export public key so clients can import it via repo gpgkey URL
    pubkey_file="$CREATEREPO_WEBROOT/RPM-GPG-KEY-jriver.asc"
    local pubkey_tmp
    if ! pubkey_tmp=$("${sign_prefix[@]}" mktemp); then
      err "Failed to create temp file for public key"
      return 1
    fi
    if ! execute "${sign_prefix[@]}" gpg --batch --yes --armor --output "$pubkey_tmp" --export "$SIGN_KEY"; then
      rm -f "$pubkey_tmp"
      err "Public key export failed for SIGN_KEY=$SIGN_KEY"
      return 1
    fi
    execute sudo install -m 0644 "$pubkey_tmp" "$pubkey_file"
    execute sudo chown "$WEBROOT_USER:$WEBROOT_USER" "$pubkey_file"
    rm -f "$pubkey_tmp"
  fi
}

# @description Symlink certificates if they do not exist in default location
link_ssl_certs() {
  debug "${FUNCNAME[0]}()"
  local target_cert f
  local mc_cert_link="$MC_ROOT/ca-certificates.crt"
  local -a source_certs=(
    "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" 
    "/var/lib/ca-certificates/ca-bundle.pem"
    "$MC_ROOT/local-ca-certificates.crt")

  target_cert=$(readlink -f "$mc_cert_link")
  [[ -f $target_cert ]] && return 0

  for f in "${source_certs[@]}"; do
    if [[ -f $f ]]; then
      if execute sudo ln -fs "$f" "$mc_cert_link"; then
        echo "Symlinked $mc_cert_link to $f"
        return 0
      fi
    fi
  done
  err "Certificate symlinking failed"; return 1
}

# @description Restore the mjr license file from MJR_FILE or other common locations
restore_license() {
  debug "${FUNCNAME[0]}()"
  local newest f
  local -a mjrfiles

  # Glob mjr files from common directories
  shopt -s nullglob
  mjrfiles=(
    "$SCRIPT_DIR"/*.mjr 
    "$OUTPUT_DIR"/*.mjr 
    "$HOME"/[dD]ownloads/*.mjr
    "$HOME"/[dD]ocuments/*.mjr
  )
  shopt -u nullglob

  if [[ ${#mjrfiles[@]} -gt 0 ]]; then

    debug "mjrfiles=(${mjrfiles[*]})"

    # Sort globbed files by time, newest first
    newest=${mjrfiles[0]}
    for f in "${mjrfiles[@]}"; do
      if [[ -f $f && $f -nt $newest ]]; then
        newest=$f
      fi
    done

    debug "Latest mjrfile: $newest"

    for f in "$MJR_FILE" "$newest"; do
      if [[ -f $f ]]; then
        if execute "mediacenter$MC_MVERSION" "/RestoreFromFile" "$f"; then
          echo "Restored license from $f"
          return 0
        else
          err "Failed to restore license from $f"
        fi
      fi
    done
    return 1
  fi
}

# @description Opens ports using the system firewall tool
# @arg $1 string Service name
# @arg $2 array List of ports in firewall-cmd format
open_firewall() {
  debug "${FUNCNAME[0]}()" "$@"
  local service="$1"
  shift
  local -a f_ports=("$@") # for firewall-cmd
  local u_ports="$*"
  u_ports="${u_ports// /|}" # concatenate
  u_ports="${u_ports//-/\:}" # for ufw
  local port

  if command -v firewall-cmd &>/dev/null; then
    if ! sudo firewall-cmd --get-services | grep -q "$service"; then
      execute sudo firewall-cmd --permanent "--new-service=$service"
      execute sudo firewall-cmd --permanent "--service=$service" "--set-description=$service installed by installJRMC"
      execute sudo firewall-cmd --permanent "--service=$service" "--set-short=$service"
      for port in "${f_ports[@]}"; do
        execute sudo firewall-cmd --permanent "--service=$service" "--add-port=$port"
      done
      execute sudo firewall-cmd --add-service "$service" --permanent
      execute sudo firewall-cmd --reload
    fi
  elif command -v ufw &>/dev/null; then
    sudo bash -c "cat <<-EOF > /etc/ufw/applications.d/$service
			[$service]
			title=$service
			description=$service installed by installJRMC
			ports=$u_ports
		EOF"
    execute sudo ufw app update "$service"
    execute sudo ufw allow "$service"
  else
    return 1
  fi
}

# @description Create the xvnc or x11vnc password file
# @arg $1 string Service type (xvnc, x11vnc)
set_vnc_pass() {
  debug "${FUNCNAME[0]}()"
  local vncpassfile="$HOME/.vnc/jrmc_passwd"

  [[ -d ${vncpassfile%/*} ]] || execute mkdir -p "${vncpassfile%/*}"

  if [[ -f $vncpassfile ]]; then
    if [[ ! -v VNCPASS ]]; then
      err "Refusing to overwrite existing $vncpassfile with an empty password"
      err "Remove existing $vncpassfile or use --vncpass ''"
      return 1
    else
      execute rm -f "$vncpassfile"
    fi
  fi

  if [[ -v VNCPASS ]]; then
    if [[ $1 == "xvnc" ]]; then
      echo "$VNCPASS" | vncpasswd -f > "$vncpassfile"
    elif [[ $1 == "x11vnc" ]]; then
      execute x11vnc -storepasswd "$VNCPASS" "$vncpassfile"
    fi
    return
  else
    declare -gi NOVNCAUTH=1
  fi
}

# @description Set display and port variables
set_display_vars() {
  debug "${FUNCNAME[0]}()"
  declare -g THIS_DISPLAY THIS_DISPLAY_NUM NEXT_DISPLAY

  # Check USER_DISPLAY, else environment DISPLAY, else set to :0
  THIS_DISPLAY="${USER_DISPLAY:-${DISPLAY:-:0}}"
  THIS_DISPLAY_NUM="${THIS_DISPLAY#*:}" # strip prefix
  THIS_DISPLAY_NUM="${THIS_DISPLAY_NUM%%.*}" # strip suffix
  # Increment each time we run this
  if ((NEXT_DISPLAY_NUM)); then
    declare -g NEXT_DISPLAY_NUM=$((NEXT_DISPLAY_NUM + 1))
  else
    declare -g NEXT_DISPLAY_NUM=$((THIS_DISPLAY_NUM + 1))
  fi
  NEXT_DISPLAY=":$NEXT_DISPLAY_NUM"
}

# @description Create associated service variables based on service name
# @arg $1 string Service name
set_service_vars() {
  debug "${FUNCNAME[0]}()" "$@"
  declare -g SERVICE_NAME SERVICE_FNAME TIMER_NAME TIMER_FNAME 
  declare -g USER_STRING GRAPHICAL_TARGET
  declare -ga RELOAD ENABLE DISABLE IS_ENABLED IS_ACTIVE
  local -a systemctl_prefix
  local service_name="$1"
  local service_type="${SERVICE_TYPE:-${2:-system}}"
  local service_dir="/usr/lib/systemd/$service_type"

  if [[ $USER == "root" && $service_type == "user" ]]; then
    err "Trying to install user service as root"
    err "Use --service-type service and/or execute installJRMC as non-root user"
    return 1
  fi

  if [[ $service_type == "system" ]]; then
    systemctl_prefix=(sudo systemctl)
    GRAPHICAL_TARGET="graphical.target"
  elif [[ $service_type == "user" ]]; then
    systemctl_prefix=(systemctl --user)
    GRAPHICAL_TARGET="default.target"
  fi

  # systemctl commands
  RELOAD=("${systemctl_prefix[@]}" daemon-reload)
  ENABLE=("${systemctl_prefix[@]}" enable --now)
  DISABLE=("${systemctl_prefix[@]}" disable --now)
  IS_ENABLED=("${systemctl_prefix[@]}" is-enabled --quiet)
  IS_ACTIVE=("${systemctl_prefix[@]}" is-active --quiet)

  [[ -d $service_dir ]] || execute sudo mkdir -p "$service_dir"

  if [[ $service_type == "system" && $USER != "root" ]]; then
    SERVICE_FNAME="$service_dir/$service_name@.service"
    TIMER_FNAME="$service_dir/$service_name@.timer"
    SERVICE_NAME="$service_name@$USER.service"
    TIMER_NAME="$service_name@$USER.timer"
    USER_STRING="User=%I"
  else
    SERVICE_NAME="$service_name.service"
    TIMER_NAME="$service_name.timer"
    SERVICE_FNAME="$service_dir/$SERVICE_NAME"
    TIMER_FNAME="$service_dir/${TIMER_NAME}"
    USER_STRING=""
  fi
}

# @section Services
# @description Starts and enables (at startup) a JRiver Media Center service
# @arg $1 string Passes arguments as startup options to /usr/bin/mediacenter??
service_jriver-mediacenter() {
  debug "${FUNCNAME[0]}()"

  set_service_vars "${FUNCNAME[0]##*_}" "user"

  sudo bash -c "cat <<-EOF > $SERVICE_FNAME
		[Unit]
		Description=JRiver Media Center $MC_MVERSION
		After=$GRAPHICAL_TARGET

		[Service]
		Type=simple
		$USER_STRING
		ExecStart=/usr/bin/mediacenter$MC_MVERSION $*
		KillMode=none
		ExecStop=/usr/bin/mc$MC_MVERSION /MCC 20007
		Restart=always
		RestartSec=10
		TimeoutStopSec=30

		[Install]
		WantedBy=$GRAPHICAL_TARGET
	EOF"

  open_firewall "jriver-mediacenter" "52100-52200/tcp" "1900/udp"

  "${RELOAD[@]}" &&
  "${ENABLE[@]}" "$SERVICE_NAME"
}

# @description Starts and enables (at startup) a JRiver Media Server service
service_jriver-mediaserver() {
  debug "${FUNCNAME[0]}()"
  set_service_vars "${FUNCNAME[0]##*_}" "user"
  service_jriver-mediacenter "/MediaServer"
}

# @description Starts and enables (at startup) JRiver Media Center in a new Xvnc session
# TODO https://github.com/TigerVNC/tigervnc/blob/master/unix/vncserver/HOWTO.md
service_jriver-xvnc() {
  debug "${FUNCNAME[0]}()"
  local -a start_cmd

  set_service_vars "${FUNCNAME[0]##*_}" "system"
  set_display_vars
  declare -g PORT=$((NEXT_DISPLAY_NUM + 5900))

  install_package tigervnc-server
  set_vnc_pass xvnc

  start_cmd=(
    /usr/bin/vncserver "$NEXT_DISPLAY"
    -geometry 1440x900 
    -alwaysshared
    -autokill
    -xstartup "/usr/bin/mediacenter$MC_MVERSION"
  )

  if ((NOVNCAUTH)); then
    start_cmd+=(
      -name "jriver$NEXT_DISPLAY" 
      -SecurityTypes None)
  else
    start_cmd+=(
      -rfbauth "$HOME/.vnc/jrmc_passwd")
  fi

  sudo bash -c "cat <<-EOF > $SERVICE_FNAME
		[Unit]
		Description=Remote desktop service (VNC)
		After=multi-user.target

		[Service]
		Type=forking
		$USER_STRING
		ExecStartPre=/bin/sh -c '/usr/bin/vncserver -kill $NEXT_DISPLAY &>/dev/null || :'
		ExecStart=${start_cmd[*]}
		ExecStop=/usr/bin/vncserver -kill $NEXT_DISPLAY
		Restart=always

		[Install]
		WantedBy=multi-user.target
	EOF"

  "${RELOAD[@]}"

  if ! "${ENABLE[@]}" "$SERVICE_NAME"; then
    err "vncserver failed to start on DISPLAY $NEXT_DISPLAY"
    # Allow to increment 10 times before breaking
    max=$((THIS_DISPLAY_NUM + 10))
    while [[ $NEXT_DISPLAY_NUM -lt $max ]]; do
      echo "Incrementing DISPLAY and retrying"
      service_jriver-xvnc && return
    done
    return 1
  else
    echo "Xvnc running on localhost:$PORT"
    open_firewall "jriver-xvnc" "$PORT/tcp"
    open_firewall "jriver-mediacenter" "52100-52200/tcp" "1900/udp"
    return 0
  fi
}

# @description Starts and enables (at startup) x11vnc screen sharing for the local desktop
service_jriver-x11vnc() {
  debug "${FUNCNAME[0]}()"
  local -a start_cmd
  set_service_vars "${FUNCNAME[0]##*_}" "user"
  set_display_vars
  declare -g PORT=$((THIS_DISPLAY_NUM + 5900))
  install_package x11vnc
  set_vnc_pass x11vnc

  # If .Xauthority file is missing, generate a dummy for x11vnc -auth guess
  if [[ ! -f "$HOME/.Xauthority" ]]; then
    [[ $XDG_SESSION_TYPE == "wayland" ]] && 
    ask_ok "Unsupported Wayland session detected for x11vnc, continue?" || return 1
    debug "Generating $HOME/.Xauthority"
    execute touch "$HOME/.Xauthority"
    execute chmod 644 "$HOME/.Xauthority"
    xauth generate "$DISPLAY" . trusted
    xauth add "$HOST$DISPLAY" . "$(xxd -l 16 -p /dev/urandom)"
  fi

  start_cmd=(
    /usr/bin/x11vnc 
    -display "$DISPLAY"
    -noscr
    -auth guess
    -forever
    -bg
  )

  if ((NOVNCAUTH)); then
    start_cmd+=(-nopw)
  else
    start_cmd+=(-rfbauth "$HOME/.vnc/jrmc_passwd")
  fi

  sudo bash -c "cat <<-EOF > $SERVICE_FNAME
		[Unit]
		Description=x11vnc
		After=$GRAPHICAL_TARGET

		[Service]
		$USER_STRING
		Type=forking
		Environment=DISPLAY=$DISPLAY
		ExecStart=${start_cmd[*]}
		Restart=always
		RestartSec=10

		[Install]
		WantedBy=$GRAPHICAL_TARGET
	EOF"

  open_firewall "jriver-x11vnc" "$PORT/tcp"

  "${RELOAD[@]}" &&
  "${ENABLE[@]}" "$SERVICE_NAME" && 
  echo "x11vnc running on localhost:$PORT"
}

# @description Starts and enables (at startup) an hourly service to build the latest version of
# JRiver Media Center RPM from the source DEB and create/update an RPM repository
service_jriver-createrepo() {
  debug "${FUNCNAME[0]}()"
  local -a sign_args start_cmd
  local service_script start_cmd

  CREATEREPO_SWITCH=0 # skip running createrepo when generating service

  set_service_vars "${FUNCNAME[0]##*_}" "system"

  # jriver-createrepo must run as root, not templated per-user
  SERVICE_NAME="jriver-createrepo.service"
  TIMER_NAME="jriver-createrepo.timer"
  SERVICE_FNAME="/usr/lib/systemd/system/jriver-createrepo.service"
  TIMER_FNAME="/usr/lib/systemd/system/jriver-createrepo.timer"
  USER_STRING=""

  # System services cannot exec files from home directories (SELinux).
  # If the script lives under /home/, copy it to a system path first.
  if [[ $SCRIPT_PATH == /home/* ]]; then
    service_script="/opt/installJRMC/installJRMC"
    echo "Script is in a home directory; installing to $service_script for system service"
    if ! { execute sudo mkdir -p "/opt/installJRMC" && execute sudo install -m 0755 "$SCRIPT_PATH" "$service_script"; }; then
      err "Could not install script to $service_script; the service may fail to start"
      service_script="$SCRIPT_PATH"
    fi
  else
    service_script="$SCRIPT_PATH"
  fi

  sign_args=()
  ((SIGN_SWITCH)) && sign_args+=(--sign)
  [[ -n $SIGN_USER ]] && sign_args+=(--sign-user="$SIGN_USER")
  [[ -n $SIGN_KEY ]] && sign_args+=(--sign-key="$SIGN_KEY")

  start_cmd=(
    "$service_script"
    --outputdir="$OUTPUT_DIR"
    --createrepo="$CREATEREPO_TARGET"
    --createrepo-webroot="$CREATEREPO_WEBROOT"
    --webroot-user="$WEBROOT_USER"
    --mcrepo="$MC_REPO"
    "${sign_args[@]}"
    --yes
    --no-update
  )

  # Pass --debug to service file if it was set for the main script
  ((DEBUG)) && start_cmd+=("--debug")

  debug "ExecStart=${start_cmd[*]}"

  sudo bash -c "cat <<-EOF > $SERVICE_FNAME
		[Unit]
		Description=Builds JRiver Media Center RPM, moves it to the repo dir, and runs createrepo

		[Service]
		ExecStart=${start_cmd[*]}

		[Install]
		WantedBy=multi-user.target
	EOF"

  sudo bash -c "cat <<-EOF > $TIMER_FNAME
		[Unit]
		Description=Run JRiver MC rpmbuild hourly

		[Timer]
		OnCalendar=hourly
		Persistent=true

		[Install]
		WantedBy=timers.target
	EOF"

  "${RELOAD[@]}" &&
  "${ENABLE[@]}" "$TIMER_NAME"
}

# @description Fixes $HOME/.jriver directory permissions
fix_dotjriver_permissions() {
  debug "${FUNCNAME[0]}()"

  # Ensure the user owns their .jriver directory
  if [[ -d "$HOME/.jriver" ]]; then
    local owner
    owner=$(stat -c '%U' "$HOME/.jriver")
    if [[ "$owner" != "$USER" ]]; then
      ask_ok "$USER does not currently own $HOME/.jriver, attempt fix?" &&
      execute sudo chown -R "$USER:$USER" "$HOME/.jriver"
    fi
  fi
}

# @description Completely uninstalls MC, services, and firewall rules
uninstall() {
  debug "${FUNCNAME[0]}()"
  local service type unit f

  if ! ask_ok "Uninstall JRiver Media Center, services, and firewall rules?"; then
    echo "Uninstall cancelled"
    return 1
  fi

  echo "Stopping and removing all Media Center services"
  for service in $(compgen -A "function" "service"); do
    service="${service##service_}"
    for type in user system; do
      set_service_vars "$service" "$type"; 
      for unit in "$SERVICE_NAME" "$TIMER_NAME"; do
        if "${IS_ACTIVE[@]}" "$unit" || 
        "${IS_ENABLED[@]}" "$unit" &>/dev/null; then
          "${DISABLE[@]}" "$unit"
        fi
      done
      for f in "$SERVICE_FNAME" "$TIMER_FNAME"; do
        [[ -f $f ]] && execute sudo rm -f "$f"
      done
      "${RELOAD[@]}"
    done
  done

  echo "Removing firewall rules"
  for service in jriver-mediacenter jriver-xvnc jriver-x11vnc; do
    if command -v firewall-cmd &>/dev/null; then
      execute sudo firewall-cmd --permanent --remove-service=$service
      execute sudo firewall-cmd --permanent --delete-service=$service
      execute sudo firewall-cmd --reload
    elif command -v ufw &>/dev/null; then
      execute sudo ufw delete allow $service
      [[ -f /etc/ufw/applications.d/$service ]] &&
        execute sudo rm -f /etc/ufw/applications.d/$service
    fi
  done

  echo "Uninstalling the JRiver Media Center package"
  if "${PKG_REMOVE[@]}" "${MC_PKG%%=*}"; then # strip version specifier
    echo "Successfully uninstalled the ${MC_PKG%%=*} package"
  elif [[ $? -eq 100 ]]; then
    err "JRiver Media Center package '${MC_PKG%%=*}' is not present and was not uninstalled"
  else
    err "Could not remove Media Center package"
  fi

  # Remove the repository files
  for file in "/etc/yum.repos.d/jriver.repo" /etc/apt/sources.list.d/{jriver,mediacenter}*.{list,sources}; do
    if [[ -e $file ]]; then
      echo "Removing repository file: $file"
      execute sudo rm -f "$file"
    fi
  done

  [[ $ID == "suse" ]] && execute sudo zypper --non-interactive removerepo jriver

  local keyfile="/usr/share/keyrings/jriver-com-archive-keyring.gpg"
  if [[ -f $keyfile ]]; then
    echo "Removing the JRiver Media Center GPG key"
    execute sudo rm -f "$keyfile"
  fi

  if [[ -f $SCRIPT_DIR/.uninstall ]]; then
    echo "Removing files from .uninstall log"
    while read -r p; do
      [[ -d $p ]] && execute sudo rm -rf "$p"
    done < "$SCRIPT_DIR/.uninstall"
    mv "$SCRIPT_DIR/.uninstall" "$SCRIPT_DIR/.uninstall.bk"
  fi

  if [[ -d $OUTPUT_DIR ]]; then
    if ask_ok "Remove installJRMC output directory $OUTPUT_DIR?"; then
      execute sudo rm -rf "$OUTPUT_DIR"
    fi
  fi

  if [[ -d $MC_ROOT ]]; then
    if ask_ok "Remove MC installation directory $MC_ROOT?"; then
      execute sudo rm -rf "$MC_ROOT"
    fi
  fi

  if [[ -d $HOME/.jriver ]]; then
    if ask_ok "Backup and reset your MC library?"; then
      execute mv "$HOME/.jriver" "$HOME/.jriver.bk"
      echo "Your MC library has been reset and backed up to $HOME/.jriver.bk"
      echo "To restore your MC library: mv $HOME/.jriver.bk $HOME/.jriver"
      return
    fi
    echo "To backup and reset your MC library: mv $HOME/.jriver $HOME/.jriver.bk"
    echo "To remove your MC library: rm -rf $HOME/.jriver"
  fi
}

# @description Checks for installJRMC update and re-executes, if necessary
update() {
  debug "${FUNCNAME[0]}()" "$@"
  debug "Checking for installJRMC update"

  # Extract and normalize version from a script
  extract_version() {
    local version_line
    version_line=$(grep -m 1 'SCRIPT_VERSION=' "$1")
    version_line=${version_line#*=}
    version_line=${version_line#\"}
    version_line=${version_line%-dev\"}
    version_line=${version_line%\"}
    echo "$version_line"
  }

  # Compare semantic version strings
  version_greater() {
    [[ "$(echo -e "$1\n$2" | sort -V | head -n 1)" != "$1" ]]
  }

  # Check if we're in a git directory and if it's the installJRMC repository
  if git -C "$SCRIPT_DIR" rev-parse --is-inside-work-tree &>/dev/null &&
  [[ "$(git -C "$SCRIPT_DIR" config --get remote.origin.url)" =~ installJRMC|installjrmc ]]; then

    # Get the current commit hash
    local before_pull_hash
    before_pull_hash=$(git -C "$SCRIPT_DIR" rev-parse HEAD)

    # Stash local changes before pull
    execute git -C "$SCRIPT_DIR" stash push --quiet
    
    # Pull latest changes
    execute git -C "$SCRIPT_DIR" pull --quiet

    # Restore local changes
    execute git -C "$SCRIPT_DIR" stash pop --quiet

    debug "Current commit hash: $before_pull_hash"
    debug "New commit hash: $(git -C "$SCRIPT_DIR" rev-parse HEAD)"

    # If the commit hash has changed, an update occurred
    if [[ "$before_pull_hash" != $(git -C "$SCRIPT_DIR" rev-parse HEAD) ]]; then
      echo "Detected installJRMC update, restarting"
      rerun "$@"
    fi
  else
    debug "Not in the installJRMC repository, checking for installJRMC update via webscrape."

    local tmp
    tmp=$(mktemp) || { err "Failed to create temporary file."; return 1; }

    # Acquire the latest version of the script
    if ! download "$SCRIPT_URL" "$tmp"; then
      err "Failed to download the latest script."
      execute rm -f "$tmp"
      return 1
    fi

    # Extract the latest version number
    local remote_version
    remote_version=$(extract_version "$tmp")
    if [[ -z "$remote_version" ]]; then
      err "Failed to extract version from the downloaded script."
      execute rm -f "$tmp"
      return 1
    fi

    # Compare versions and update if the remote version is greater
    if version_greater "$remote_version" "$SCRIPT_VERSION"; then
      execute mv "$tmp" "$SCRIPT_PATH" || { err "Failed to replace the script"; execute rm -f "$tmp"; return 1; }
      execute chmod +x "$SCRIPT_PATH" || { err "Failed to make the script executable"; return 1; }
      execute rm -f "$tmp"

      echo "Detected installJRMC update, restarting"
      rerun "$@"
    else
      debug "Current installJRMC $SCRIPT_VERSION is the latest version"
      execute rm -f "$tmp"
      return 0
    fi
  fi
}

# @description installJRMC main function
main() {
  debug "${FUNCNAME[0]}()" "$@" # prints function name and arguments

  echo "Starting installJRMC $SCRIPT_VERSION"

  if ((DEBUG)); then
    echo "Debugging on"
  else
    echo "To enable debugging output, use --debug or -d"
  fi

  # Parse input, set default/host variables, and MC version
  init "$@"

  ((UNINSTALL_SWITCH)) && uninstall

  # Exit now if only --uninstall is passed
  if ((UNINSTALL_SWITCH)) && 
     ! ((BUILD_SWITCH || CREATEREPO_SWITCH || REPO_INSTALL_SWITCH || LOCAL_INSTALL_SWITCH)) &&
     [[ ${#SERVICES[@]} -eq 0 && ${#CONTAINERS[@]} -eq 0 ]]; then
    exit 0
  fi

  if ((REPO_INSTALL_SWITCH)); then
    echo "Installing JRiver Media Center from remote repository"
    local repo_file

    install_external_repos

    case $ID in
      fedora|centos)
        local keyurl="https://repos.bryanroessler.com/jriver/RPM-GPG-KEY-jriver.asc"
        # local keyfile="/etc/pki/rpm-gpg/RPM-GPG-KEY-jriver"

        echo "Installing repository file: $repo_file"
        repo_file="/etc/yum.repos.d/jriver.repo"
        sudo tee "$repo_file" &>/dev/null <<-EOF
					[jriver]
					baseurl = https://repos.bryanroessler.com/jriver
					enabled = 1
					gpgcheck = 1
					repo_gpgcheck = 1
					gpgkey = $keyurl
					name = JRiver Media Center hosted by BryanC
				EOF
        ;;
      debian|ubuntu)
        local keyurl="https://dist.jriver.com/mediacenter@jriver.com.gpg.complete"
        local keyfile="/usr/share/keyrings/jriver-com-archive-keyring-complete.gpg"
        local channel="latest"
        [[ -n $BETAPASS ]] && channel="beta"

        if [[ $channel == "beta" ]]; then
          repo_file="/etc/apt/sources.list.d/jriver-beta.sources"
        else
          repo_file="/etc/apt/sources.list.d/jriver.sources"
        fi

        echo "Installing JRiver Media Center GPG key"
        download "$keyurl" "-" | 
          gpg --dearmor | sudo tee "$keyfile" &>/dev/null

        echo "Installing repository file: $repo_file"
        sudo tee "$repo_file" &>/dev/null <<-EOF
					Types: deb
					URIs: https://dist.jriver.com/$channel/mediacenter/
					Signed-By: $keyfile
					Suites: $MC_REPO
					Components: main
					Architectures: amd64 armhf arm64
				EOF

        add_legacy_repo
        ;;
      *) 
        err "An MC repository for $ID is not yet available"
        err "Use --install=local to install MC on $ID"
        return 1
        ;;
    esac

    echo "Updating package lists"
    if ! "${PKG_UPDATE[@]}"; then
      err "Package update failed!"
      remove_legacy_repo
      if [[ $MC_REPO != "$MC_REPO_HARDCODE" ]]; then
        echo "Rerunning installJRMC with --mcrepo=$MC_REPO_HARDCODE"
        rerun "$@" "--mcrepo=$MC_REPO_HARDCODE"
      fi
      return 1
    fi

    echo "Installing $MC_PKG package"
    if install_package --no-install-check --allow-downgrades "$MC_PKG"; then
      echo "Successfully installed JRiver Media Center from repository"
    else
      err "MC failed to install"
      remove_legacy_repo
      return 1
    fi

    # Clean up legacy repo after successful install
    remove_legacy_repo

    link_ssl_certs
    restore_license
    open_firewall "jriver-mediacenter" "52100-52200/tcp" "1900/udp"
  fi

  if ((BUILD_SWITCH)); then
    acquire_deb || { err "Could not download Media Center DEB package"; return 1; }

    # Convert the source DEB dependencies to various distro-specific packages
    install_package dpkg
    translate_packages "$MC_DEB" requires recommends

    if [[ $BUILD_TARGET =~ centos|fedora|suse|mandriva || $CREATEREPO_TARGET =~ centos|fedora|suse|mandriva ]]; then
      install_package rpm-build
      [[ -d $OUTPUT_DIR/SPECS ]] || execute mkdir -p "$OUTPUT_DIR/SPECS"
      if build_rpm requires recommends; then
        echo "RPM package built successfully"
      else
        err "Failed to build RPM package"
        # On build failure, remove the source DEB in case it is corrupted
        if [[ -f $MC_DEB ]]; then
          echo "Removing source DEB"
          if ! execute rm -f "$MC_DEB"; then
            execute sudo rm -f "$MC_DEB"
          fi
        fi
        return 1
      fi
    elif [[ $BUILD_TARGET =~ arch ]]; then
      if build_pkgbuild requires recommends; then
        echo "Successfully generated Arch PKGBUILD"
      fi
    fi
  fi

  if ((LOCAL_INSTALL_SWITCH)); then
    echo "Installing JRiver Media Center from local package"

    install_external_repos

    # Install MC package
    case $ID in
      fedora|centos|mandriva|suse) 
        local -a gpg_flag; ((SIGN_SWITCH)) || gpg_flag=(--no-gpg-check)
        install_package --no-install-check "${gpg_flag[@]}" --allow-downgrades "$MC_RPM"
        ;;
      debian|ubuntu) install_mc_deb "$@" ;;
      arch) install_mc_arch ;;
      unknown) install_mc_generic ;;
    esac

    # shellcheck disable=SC2181
    if [[ $? -eq 0 ]]; then
      echo "Successfully installed JRiver Media Center from local package"
    else
      err "MC package install failed!"
    fi
    
    link_ssl_certs
    restore_license
    open_firewall "jriver-mediacenter" "52100-52200/tcp" "1900/udp"
  fi

  if [[ ${#SERVICES[@]} -gt 0 ]]; then
    declare service
    for service in "${SERVICES[@]}"; do
      if ! "service_$service"; then
        if [[ $? -eq 127 ]]; then
          err "Service $service does not exist, check service name"
        else
          err "Failed to create $service service"
        fi
      else
        echo "Started and enabled $service service"
      fi
    done
    unset service
  fi

  if ((CREATEREPO_SWITCH)); then
    if run_createrepo; then
      echo "Successfully updated repo"
    else
      err "Repo creation failed"
    fi
  fi
}

# @section Helper functions
# @internal
debug() { ((DEBUG)) && echo "Debug: $*"; }
err() { echo "Error: $*" >&2; }
ask_ok() {
  local response
  ((YES_SWITCH)) && return 0
  read -n 1 -r -p "$* [y/N]: " response
  echo
  [[ ${response,,} == y ]]
}
execute() {
  if debug "$*"; then
    "$@"
  else
    "$@" &>/dev/null
  fi
}
fix_permissions() {
  local dir="$1"
  local user="$2"
  local owner
  [[ -d "$dir" ]] || return 1
  owner=$(stat -c '%U' "$dir")
  if [[ "$owner" != "$user" ]]; then
    echo "Directory $dir is owned by $owner, not $user"
    if ask_ok "Change ownership of $dir to $user?"; then
      execute sudo chown -R "$user:$user" "$dir"
    fi
  fi
}
download() {
  debug "${FUNCNAME[0]}()" "$@"
  local url="$1"
  local output="${2:-}"
  local -a cmd
  if command -v curl &>/dev/null || install_package curl; then
    cmd=(curl --silent --fail --location)
    if [[ -n "$output" ]]; then
      cmd+=(--output "$output")
    else
      cmd+=(--remote-name)
    fi
  elif command -v wget &>/dev/null || install_package wget; then
    cmd=(wget --quiet)
    [[ -n "$output" ]] && cmd+=("--output-document=$output")
  else
    err "Unable to install wget or curl"
    return 1
  fi
  debug "${cmd[@]}" "$url"
  "${cmd[@]}" "$url"
}
create_mc_apt_container() {
  debug "${FUNCNAME[0]}()" "$@"
  declare -g CNT
  local -a cmds=("$@")
  local repo="$MC_REPO"
  local channel="latest"
  
  if [[ -n $BETAPASS ]]; then
    repo="$MC_REPO_HARDCODE"
    channel="beta"
  fi

  debug "Using MC $repo repo $channel channel for apt container"

  # shellcheck disable=SC2016
  { command -v buildah &>/dev/null || install_package buildah; } &&
  CNT=$(buildah from --quiet alpine:edge) &&
  buildah run --env MC_REPO="$repo" --env MC_ARCH="$MC_ARCH" --env CHANNEL="$channel" "$CNT" -- sh -c '
    apk add --quiet --no-progress --no-cache apt curl gnupg
    curl -fsSL https://dist.jriver.com/mediacenter@jriver.com.gpg.complete | gpg --quiet --dearmor -o /usr/share/keyrings/jriver-com-archive-keyring.gpg
    cat <<-EOF > /etc/apt/sources.list.d/jriver.sources
			Types: deb
			URIs: https://dist.jriver.com/$CHANNEL/mediacenter/
			Signed-By: /usr/share/keyrings/jriver-com-archive-keyring.gpg
			Suites: $MC_REPO
			Components: main
			Architectures: $MC_ARCH
		EOF
    apt-get update -qq' &&
  # If user passes command strings run them in the container
  for cmd in "${cmds[@]}"; do
    buildah run "$CNT" -- sh -c "$cmd" || { err "$cmd failed"; return 1; }
  done
}
rerun() {
  debug "${FUNCNAME[0]}()" "$@"
  if ((SCRIPT_IS_PIPED)); then
    # Re-download and execute if script was piped
    curl -fsSL "$SCRIPT_URL" | bash -s -- "$@" "--no-update"
    exit $?
  else
    exec bash "$SCRIPT_PATH" "$@" "--no-update"
  fi
}

# Roughly turn debugging on for pre-init
# Reset and reparse in parse_input() with getopt
[[ " $* " =~ ( --debug | -d ) ]] && DEBUG=1

main "$@"
exit
