#!/usr/bin/env bash
# shellcheck disable=SC1091
. gettext.sh

usage() {
  cat <<EOF
$(gettext "Usage: downgrade [option...] <pkg> [pkg...] [-- pacman_option...]")

$(gettext "Options"):
  --pacman        <$(gettext "command")>
                  $(gettext "pacman command to use, defaults to") "pacman"
  --pacman-conf   <$(gettext "path")>
                  $(gettext "pacman configuration file, defaults to") "/etc/pacman.conf"
  --pacman-cache  <$(gettext "path")>
                  $(gettext "pacman cache directory,")
                  $(gettext "default value(s) taken from pacman configuration file,")
                  $(gettext "or otherwise defaults to") "/var/cache/pacman/pkg"
  --pacman-log    <$(gettext "path")>
                  $(gettext "pacman log file,")
                  $(gettext "default value taken from pacman configuration file,")
                  $(gettext "or otherwise defaults to") "/var/log/pacman.log"
  --maxdepth      <$(gettext "integer")>
                  $(gettext "maximum depth to search for cached packages, defaults to") 1
  --ala-url       <url>
                  $(gettext "location of ALA server, defaults to") "https://archive.archlinux.org"
  --ala-only      $(gettext "only use ALA server")
  --cached-only   $(gettext "only use cached packages")
  --ignore        <prompt|always|never>
                  $(gettext "whether to add packages to IgnorePkg")
  --unignore      $(gettext "remove packages from IgnorePkg")
  --latest        $(gettext "pick latest matching version")
  --oldest        $(gettext "pick oldest matching version")
  --prefer-cache  $(gettext "do not query ala if a matching package was found in cache")
  --version       $(gettext "show downgrade version")
  -h, --help      $(gettext "show help script")

$(gettext "Note"):
  $(gettext "Options after the -- characters will be treated as pacman options.")
  $(gettext "See downgrade(8) for details.")
EOF
}

sed_msg() {
  local msg="$1"
  shift

  if ! sed "$@" 2>/dev/null; then
    printf "%s\n" "Failed $msg" >&2
    exit 1
  fi
}

read_pacman_conf() {
  pacman-conf --config "$PACMAN_CONF" "$1"
}

read_downgrade_conf() {
  local var=$1

  eval "$var=($(grep -E -v '^ *(#.*)?$' "$DOWNGRADE_CONF" 2>/dev/null | xargs printf '%q '))"
}

read_unique() {
  local var=$1
  shift

  if [[ -n "$*" ]]; then
    # shellcheck disable=SC2229
    mapfile -t "$var" < <(printf "%s\n" "$@" | sort -u)
  fi
}

previously_installed() {
  # Delay this defaulting so #read_pacman_conf behavior is tested
  : "${PACMAN_LOG:=$(read_pacman_conf LogFile)}"
  : "${PACMAN_LOG:=/var/log/pacman.log}"

  sed_msg "to parse previously installed packages" '
    /.*\(installed\|upgraded\) \('"$1"'\) (\(.* -> \)\?\([^)]*\))/!d
    s//\2-\4/
  ' "$PACMAN_LOG"
}

currently_installed() {
  LC_ALL=C.UTF8 "$PACMAN" -Qi "$1" 2>/dev/null | awk -F " : " '
    /^Name / { name=$2 };
    /^Version / { version=$2 };
    END { if (name != "") printf("%s-%s\n", name, version) }
  '
}

# <package name> [package path] …
present_packages() {
  local i=1
  local pkgname="$1"
  shift

  (($#)) || return 1

  repo="$(pacman -Ss "^${pkgname}$" | awk -F'/' 'NR==1 {print $1}')"
  repo=${repo:-foreign/aur}

  gettext 'Available packages'
  printf " (%s):\n" "$repo"

  for entry; do
    output_package "$((i++))" "$entry" "$pkgname"
  done | column -s '	' -t -R 2,4,6
  # Expand tabs in the output of output_package to spaces as needed to create a
  # table. Right align numerical columns 2 (index), 4 (epoch) and 6 (release).
}

read_selection() {
  local ans

  read -r ans

  ((ans > 0 && ans <= $#)) && printf "%s" "${!ans}"
}

is_yes() {
  local ans=${1,,} y

  y=$(gettext 'y')

  # Accept localized yes or english yes
  # See https://github.com/archlinux-downgrade/downgrade/issues/219
  [[ "${1,,}" == ${y}* ]] || [[ "${1,,}" == y* ]]
}

prompt_to_ignore() {
  local pkg ans

  if [[ $DOWNGRADE_IGNORE == never ]]; then
    return
  fi

  for pkg; do
    if ! pacignore check -c "$PACMAN_CONF" "$pkg"; then
      if [[ $DOWNGRADE_IGNORE == always ]]; then
        pacignore add -c "$PACMAN_CONF" "$pkg"
      else
        eval_gettext "add \$pkg to IgnorePkg? [y/N] "
        read -r ans
        if is_yes "$ans"; then
          pacignore add -c "$PACMAN_CONF" "$pkg"
        fi
      fi
    fi
  done
}

filter_packages() {
  local name=$1 operator=$2 version=$3 pkg

  while read -r pkg; do
    if matches_name_version_filter "$pkg" "$name" "$operator" "$version"; then
      echo "$pkg"
    fi
  done
}

matches_name_version_filter() {
  local pkg=$1 name=$2 operator=$3 search_version=$4 pkg_version version_regex

  if [[ -z "$operator" ]] || [[ -z "$version" ]]; then
    return 0
  fi

  version_regex="[^-]+-[0-9.]+"
  pkg_version=$(sed -r "s/.*$name-($version_regex)-(any|$DOWNGRADE_ARCH)\\.pkg\\.tar\\.(gz|xz|zst)/\1/g" <<<"$pkg")
  cmp=$(vercmp "$pkg_version" "$search_version")

  case "$operator" in
    '>=')
      ((cmp >= 0))
      ;;
    '<=')
      ((cmp <= 0))
      ;;
    '>')
      ((cmp == 1))
      ;;
    '<')
      ((cmp == -1))
      ;;
    '=~')
      [[ $pkg_version =~ $search_version ]]
      ;;
    '=')
      ((cmp == 0))
      ;;
    '==')
      ((cmp == 0))
      ;;
  esac
}

search_ala() {
  local name=$1 uriname pkgfile_re index

  # This is a *very* naive URI encoding. Packages typically don't use characters
  # that need encoding, so we'll just add them as we get bug reports. So far +
  # seems to be the only one, which makes sense. If we ever move to a "real
  # language", we can use a library for this bit.
  # shellcheck disable=SC2001
  uriname=$(sed 's/+/%2B/g' <<<"$name")

  pkgfile_re="$uriname-[^-]+-[0-9.]+-(any|$DOWNGRADE_ARCH)\\.pkg\\.tar\\.(gz|xz|zst)"
  index="$DOWNGRADE_ALA_URL/packages/${uriname:0:1}/$uriname/"

  curl --fail --silent "$index" | sed_msg "to parse A.L.A." -E '
    /.* href="('"$pkgfile_re"')".*/!d;
    s||'"$index"'\1|g; s|\+| |g; s|%|\\x|g' | xargs -0 printf "%b"
}

search_cache() {
  local name=$1 pkgfile_re index

  pkgfile_re="$name-[^-]+-[0-9.]+-(any|$DOWNGRADE_ARCH)\\.pkg\\.tar\\.(gz|xz|zst)"

  # Delay this defaulting so #read_pacman_conf behavior is tested
  if ((!${#PACMAN_CACHE[@]})); then
    mapfile -t PACMAN_CACHE < <(read_pacman_conf CacheDir)
  fi

  if ((!${#PACMAN_CACHE[@]})); then
    PACMAN_CACHE=(/var/cache/pacman/pkg/)
  fi

  # shellcheck disable=SC2086
  find -L "${PACMAN_CACHE[@]}" -maxdepth "$DOWNGRADE_MAXDEPTH" -regextype posix-extended -regex ".*/$pkgfile_re"
}

sort_packages() {
  grep -Fv 'testing/' |
    awk 'BEGIN { FS="/"; OFS="|" } { print $NF, $0 }' |
    pacsort -f -t '|' -k 1 | cut -d '|' -f 2-
}

# <number> <path> <package name>
output_package() {
  local number="$1" path="$2" pkgname="$3"
  local pkg indicator=" " location timestamp version epoch release

  if [[ -n "$current" ]] && [[ "$path" == *"$current"* ]]; then
    # Currently installed
    indicator="+"
  else
    for pkg in "${installed[@]}"; do
      case "$path" in
        *$pkg*)
          indicator="-"
          break
          ;;
      esac
    done
  fi

  # Remote or local file
  if [[ $path =~ ^/ ]]; then
    location="$(dirname "$path")"
    timestamp=$(stat -c '%y' "$path" | cut -d' ' -f1)
  else
    location="$(gettext 'remote')"
    timestamp=
  fi

  IFS=, read -r epoch version release _ < <(
    extract_version_parts "$pkgname" "$path"
  )

  printf "%s\t%s)\t%s\t%s\t%s\t%s\t%s\t%s\n" \
    "$indicator" \
    "$number" \
    "$pkgname" \
    "$epoch" \
    "$version" \
    "$release" \
    "$location" \
    "$timestamp"
}

# <package name> <package path>
extract_version_parts() {
  local pkgname=$1 path=$2

  sed '
    # Strip first path component
    s|^.*/||;

    # Strip package name
    s|^.\{'${#pkgname}'\}-\?||;

    # Strip package extension
    s|\.pkg\(\.tar\)\?\(\.[a-z0-9A-Z]\+\)\?$||;

    # (epoch:)?version(-release)?(-arch)? -> epoch,version,release,arch
    s|\(\([^:]*\):\)\?\([^-]*\)\(-\([^-]*\)\)\?\(-\(.*\)\)\?|\2,\3,\5,\7|;
  ' <<<"$path"
}

# shellcheck disable=SC2207
process_term() {
  local term=$1 name operator version candidates choice

  read -r name operator version < \
    <(sed -r "s/(.*[^<>=~])(<=|>=|<|>|=|=~|==)([^<>=~].*)/\1 \2 \3/g" <<<"$term")

  installed=($(previously_installed "$name"))
  current=$(currently_installed "$name")

  candidates=()
  if ((DOWNGRADE_FROM_CACHE)); then
    candidates+=($(search_cache "$name" | filter_packages "$name" "$operator" "$version"))
  fi
  if ((DOWNGRADE_FROM_ALA)) && { ((!DOWNGRADE_PREFER_CACHE)) || ((${#candidates[@]} == 0)); }; then
    candidates=($(search_ala "$name" | filter_packages "$name" "$operator" "$version") "${candidates[@]}")
  fi

  candidates=($(printf '%s\n' "${candidates[@]}" | sort_packages))

  if (("${#candidates[@]}" == 0)); then
    {
      gettext "No results found"
      echo
    } >&2
  elif (("${#candidates[@]}" == 1)); then
    choice=${candidates[0]}
  elif ((DOWNGRADE_TO_LATEST)); then
    # Select the most up to date package
    choice=${candidates[-1]}
  elif ((DOWNGRADE_TO_OLDEST)); then
    # Select the most out of date
    choice=${candidates[0]}
  else
    choice=$(present_packages "$name" "${candidates[@]}" |
      fzf --tac --border --header-lines 1 --tiebreak=begin |
      sed 's|[^0-9]\+\([0-9]\+\)).*|\1|' | read_selection "${candidates[@]}")
    if [[ -z "$choice" ]]; then
      {
        gettext "Invalid choice"
        echo
      } >&2
    fi
  fi

  if [[ -n "$choice" ]]; then
    to_ignore+=("$name")
    to_install+=("$choice")
    return 0
  fi

  {
    eval_gettext "Unable to downgrade \$name"
    echo
  } >&2
  return 1
}

main() {
  local term candidates choice pkg exit_code=0

  (($#)) || return 1

  for term; do
    if ! process_term "$term"; then
      exit_code=1
    fi
  done

  return $exit_code
}

parse_options() {
  while [[ -n "$1" ]]; do
    case "$1" in
      --pacman)
        if [[ -n "$2" ]]; then
          shift
          PACMAN="$1"
        else
          {
            gettext "Missing --pacman argument"
            echo
            usage
          } >&2
          exit 1
        fi
        ;;
      --pacman-conf)
        if [[ -n "$2" ]]; then
          shift
          PACMAN_CONF="$1"
        else
          {
            gettext "Missing --pacman-conf argument"
            echo
            usage
          } >&2
          exit 1
        fi
        ;;
      --ala-url)
        if [[ -n "$2" ]]; then
          shift
          DOWNGRADE_ALA_URL="$1"
        else
          {
            gettext "Missing --ala-url argument"
            echo
            usage
          } >&2
          exit 1
        fi
        ;;
      --pacman-cache)
        if [[ -n "$2" ]]; then
          shift
          PACMAN_CACHE+=("$1")
        else
          {
            gettext "Missing --pacman-cache argument"
            echo
            usage
          } >&2
          exit 1
        fi
        ;;
      --pacman-log)
        if [[ -n "$2" ]]; then
          shift
          PACMAN_LOG="$1"
        else
          {
            gettext "Missing --pacman-log argument"
            echo
            usage
          } >&2
          exit 1
        fi
        ;;
      --maxdepth)
        if [[ -n "$2" ]]; then
          shift
          DOWNGRADE_MAXDEPTH="$1"
        else
          {
            gettext "Missing --maxdepth argument"
            echo
            usage
          } >&2
          exit 1
        fi
        ;;
      --ala-only)
        DOWNGRADE_FROM_ALA=1
        DOWNGRADE_FROM_CACHE=0
        ;;
      --cached-only)
        DOWNGRADE_FROM_ALA=0
        DOWNGRADE_FROM_CACHE=1
        ;;
      --ignore)
        if [[ -n "$2" && "$2" =~ ^(prompt|always|never)$ ]]; then
          shift
          DOWNGRADE_IGNORE="$1"
        else
          {
            gettext "Missing or wrong --ignore argument"
            echo
            usage
          } >&2
          exit 1
        fi
        ;;
      --unignore)
        shift
        pacignore rm -c "$PACMAN_CONF" "$@"
        exit $?
        ;;
      --latest)
        DOWNGRADE_TO_LATEST=1
        DOWNGRADE_TO_OLDEST=0
        ;;
      --oldest)
        DOWNGRADE_TO_LATEST=0
        DOWNGRADE_TO_OLDEST=1
        ;;
      --prefer-cache)
        DOWNGRADE_PREFER_CACHE=1
        ;;
      --)
        shift
        pacman_args=("$@")
        break
        ;;
      -*)
        local current_option
        # shellcheck disable=SC2034
        current_option="$1"
        {
          eval_gettext "Unrecognized option \$current_option"
          echo
          usage
        } >&2
        exit 1
        ;;
      *)
        terms+=("$1")
        ;;
    esac
    shift
  done

  if ((!"${#terms[@]}")); then
    {
      gettext "No packages provided for downgrading"
      echo
      usage
    } >&2
    exit 1
  fi
}

cli() {
  local conf_args=()

  # Get configuration and command-line arguments and parse everything
  read_downgrade_conf conf_args
  parse_options "${conf_args[@]}" "$@"

  # Make arrays unique
  read_unique terms "${terms[@]}"
  read_unique PACMAN_CACHE "${PACMAN_CACHE[@]}"

  # Proceed with rest of workflow
  main "${terms[@]}"
  pacman -U "${pacman_args[@]}" "${to_install[@]}"
  prompt_to_ignore "${to_ignore[@]}"
}

# Set script defaults
PACMAN="pacman"
PACMAN_CONF="/etc/pacman.conf"
DOWNGRADE_ARCH="$(pacman-conf Architecture | head -n 1)"
DOWNGRADE_ALA_URL="https://archive.archlinux.org"
# Disable A.L.A. only in stable branch
if [ ! "$(pacman-mirrors -aG)" == stable ] || [ "$DOWNGRADE_FROM_ALA" = "1" ] ; then
  DOWNGRADE_FROM_ALA=1
else
  echo
  echo "Downgrading from A.L.A. is disabled on the stable branch. To override this behavior, set DOWNGRADE_FROM_ALA to 1."
  echo "See https://wiki.manjaro.org/index.php/Downgrading_packages  for more details."
  echo
  DOWNGRADE_FROM_ALA=0
fi
DOWNGRADE_FROM_CACHE=1
DOWNGRADE_MAXDEPTH=1
DOWNGRADE_CONF="/etc/xdg/downgrade/downgrade.conf"
DOWNGRADE_VERSION="11.5.4"
DOWNGRADE_IGNORE="prompt"
DOWNGRADE_TO_LATEST=0
DOWNGRADE_TO_OLDEST=0
DOWNGRADE_PREFER_CACHE=0

# Main code execution
if ((!DOWNGRADE_LIB)); then
  set -e

  locale="$(dirname "$0")"/../share/locale

  if [[ -d "$locale" ]]; then
    # Packaged installation
    TEXTDOMAINDIR=$(cd "$locale" && pwd)
  else
    # Probably testing ./downgrade
    TEXTDOMAINDIR=/usr/share/locale
  fi

  export TEXTDOMAIN=downgrade TEXTDOMAINDIR

  # Check for help CLI option
  for arg; do
    if [[ "$arg" =~ ^-h$|^--help$ ]]; then
      usage
      exit 0
    elif [[ "$arg" == '--version' ]]; then
      printf "%s\n" "$DOWNGRADE_VERSION"
      exit 0
    fi
  done

  # Check to ensure downgrade running as root
  if ((UID)); then
    {
      gettext "downgrade must be run as root"
      echo
    } >&2
    exit 1
  fi

  cli "$@"
fi
