#!/bin/sh
set -eu

# This helper is installed root-owned below /opt and is invoked through pkexec.
# Do not inherit caller-controlled command lookup, proxy, locale, or CA
# configuration into a root network/install transaction.
PATH=/usr/sbin:/usr/bin:/sbin:/bin
LC_ALL=C
LANG=C
export PATH LC_ALL LANG
unset CDPATH ENV BASH_ENV CURL_HOME GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM \
  HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy \
  no_proxy SSL_CERT_FILE SSL_CERT_DIR
umask 077

debian_channel_url='https://api.mantatech.ltd/linux/update'
arch_channel_url='https://api.mantatech.ltd/linux_arch/update'
storage_prefix='https://storage.yandexcloud.net/ocean/'
arch_storage_prefix='https://storage.yandexcloud.net/ocean/MantaTech/linux/'
package_format_marker='/opt/mantaray/libexec/mantaray-linux-package-format'
max_package_bytes=536870912

die() {
  printf 'mantaray-linux-updater: %s\n' "$*" >&2
  exit 1
}

log() {
  printf 'mantaray-linux-updater: %s\n' "$*" >&2
  if [ -x /usr/bin/logger ]; then
    /usr/bin/logger --tag mantaray-linux-updater -- "$*" || true
  fi
}

detect_package_format() {
  if [ -f "$package_format_marker" ] && [ ! -L "$package_format_marker" ]; then
    package_format="$(/bin/cat "$package_format_marker")"
    case "$package_format" in
      deb|arch) printf '%s\n' "$package_format"; return 0 ;;
      *) die 'installed package-format marker is invalid' ;;
    esac
  fi
  if [ -f /etc/arch-release ] && [ -x /usr/bin/pacman ]; then
    printf 'arch\n'
  else
    printf 'deb\n'
  fi
}

channel_url_for() {
  case "$1" in
    deb) printf '%s\n' "$debian_channel_url" ;;
    arch) printf '%s\n' "$arch_channel_url" ;;
    *) die 'unsupported Linux package format' ;;
  esac
}

package_name_for() {
  package_format="$1"
  version="$2"
  case "$package_format" in
    deb) printf 'MantaRay-v%s-amd64.deb\n' "$version" ;;
    arch) printf 'mantaray-vpn-bin-%s-1-x86_64.pkg.tar.zst\n' "$version" ;;
    *) die 'unsupported Linux package format' ;;
  esac
}

package_url_for() {
  package_format="$1"
  version="$2"
  package_name="$(package_name_for "$package_format" "$version")"
  case "$package_format" in
    deb) printf '%s%s\n' "$storage_prefix" "$package_name" ;;
    arch) printf '%s%s\n' "$arch_storage_prefix" "$package_name" ;;
    *) die 'unsupported Linux package format' ;;
  esac
}

fetch_https() {
  url="$1"
  output="$2"
  /usr/bin/curl \
    --proto '=https' \
    --tlsv1.2 \
    --fail \
    --silent \
    --show-error \
    --connect-timeout 15 \
    --max-time 600 \
    --retry 3 \
    --retry-all-errors \
    --output "$output" \
    "$url"
}

validate_manifest() {
  manifest="$1"
  output_dir="$2"
  package_format="$3"
  /usr/bin/python3 - "$manifest" "$output_dir" "$package_format" <<'PY'
import json
import pathlib
import re
import sys

manifest = pathlib.Path(sys.argv[1])
output_dir = pathlib.Path(sys.argv[2])
package_format = sys.argv[3]
if package_format not in {"deb", "arch"}:
    raise SystemExit("unsupported Linux package format")
if manifest.stat().st_size > 1024 * 1024:
    raise SystemExit("manifest exceeds 1 MiB")

with manifest.open("r", encoding="utf-8") as source:
    data = json.load(source)

if not isinstance(data, dict):
    raise SystemExit("manifest must be an object")

allowed = {
    "schema_version",
    "platform",
    "arch",
    "version",
    "version_code",
    "url",
    "sha256",
    "changelog_ru",
    "changelog_en",
    "force_update",
}
unknown = sorted(set(data) - allowed)
if unknown:
    raise SystemExit("manifest has unknown fields: " + ", ".join(unknown))

if data.get("schema_version") != 1:
    raise SystemExit("unsupported manifest schema")
if data.get("platform") != "linux" or data.get("arch") != "amd64":
    raise SystemExit("manifest targets another platform")

version = data.get("version")
if not isinstance(version, str) or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version):
    raise SystemExit("invalid application version")

version_code = data.get("version_code")
if isinstance(version_code, bool) or not isinstance(version_code, int) or version_code <= 0:
    raise SystemExit("invalid application version code")

sha256 = data.get("sha256")
if not isinstance(sha256, str) or not re.fullmatch(r"[0-9a-f]{64}", sha256):
    raise SystemExit("invalid package SHA-256")

if package_format == "arch":
    expected_url = (
        "https://storage.yandexcloud.net/ocean/MantaTech/linux/"
        f"mantaray-vpn-bin-{version}-1-x86_64.pkg.tar.zst"
    )
else:
    expected_url = (
        "https://storage.yandexcloud.net/ocean/"
        f"MantaRay-v{version}-amd64.deb"
    )
if data.get("url") != expected_url:
    raise SystemExit("unexpected package URL")

for key in ("changelog_ru", "changelog_en"):
    value = data.get(key, "")
    if not isinstance(value, str) or len(value) > 16_384:
        raise SystemExit(f"invalid {key}")
if not isinstance(data.get("force_update", False), bool):
    raise SystemExit("invalid force_update")

output_dir.mkdir(mode=0o700)
values = {
    "version": version,
    "version_code": str(version_code),
    "sha256": sha256,
    "url": data["url"],
}
for name, value in values.items():
    if "\n" in value or "\r" in value:
        raise SystemExit(f"invalid newline in {name}")
    (output_dir / name).write_text(value, encoding="ascii")
PY
}

validate_debian_package_identity() {
  package="$1"
  expected_version="$2"
  [ "$(/usr/bin/timeout --foreground 30 /usr/bin/dpkg-deb --field "$package" Package)" = 'mantaray-vpn' ] ||
    die 'package identity mismatch'
  [ "$(/usr/bin/timeout --foreground 30 /usr/bin/dpkg-deb --field "$package" Version)" = "$expected_version" ] ||
    die 'package version mismatch'
  [ "$(/usr/bin/timeout --foreground 30 /usr/bin/dpkg-deb --field "$package" Architecture)" = 'amd64' ] ||
    die 'package architecture mismatch'
  /usr/bin/timeout --foreground 30 /usr/bin/dpkg-deb --info "$package" >/dev/null ||
    die 'invalid Debian package'
}

validate_arch_package_identity() {
  package="$1"
  expected_version="$2"
  metadata="$work_dir/pkginfo"
  /usr/bin/timeout --foreground 30 /usr/bin/bsdtar -xOf "$package" .PKGINFO >"$metadata" ||
    die 'invalid Arch package'
  /usr/bin/python3 - "$metadata" "$expected_version" <<'PY'
import pathlib
import sys

metadata = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
expected = {
    "pkgname": "mantaray-vpn-bin",
    "pkgver": f"{sys.argv[2]}-1",
    "arch": "x86_64",
}
seen = {}
for raw_line in metadata.splitlines():
    if " = " not in raw_line:
        continue
    key, value = raw_line.split(" = ", 1)
    if key in expected:
        if key in seen:
            raise SystemExit(f"duplicate {key} in .PKGINFO")
        seen[key] = value
if seen != expected:
    raise SystemExit("Arch package identity mismatch")
PY
}

validate_package_identity() {
  package_format="$1"
  package="$2"
  expected_version="$3"
  case "$package_format" in
    deb) validate_debian_package_identity "$package" "$expected_version" ;;
    arch) validate_arch_package_identity "$package" "$expected_version" ;;
    *) die 'unsupported Linux package format' ;;
  esac
}

version_is_newer() {
  target="$1"
  installed="$2"
  /usr/bin/python3 - "$target" "$installed" <<'PY'
import re
import sys

def parse(value):
    if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", value):
        raise SystemExit("unsupported application version")
    return tuple(int(part) for part in value.split("."))

raise SystemExit(0 if parse(sys.argv[1]) > parse(sys.argv[2]) else 1)
PY
}

run_apt() {
  /usr/bin/timeout --foreground --signal=TERM --kill-after=30s 15m \
    /usr/bin/env DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get \
    -o DPkg::Lock::Timeout=120 "$@"
}

run_pacman() {
  deadline=$(( $(/usr/bin/date +%s) + 120 ))
  while [ -e /var/lib/pacman/db.lck ]; do
    [ "$(/usr/bin/date +%s)" -lt "$deadline" ] || return 1
    /usr/bin/sleep 1
  done
  /usr/bin/timeout --foreground --signal=TERM --kill-after=30s 15m \
    /usr/bin/pacman --noconfirm -U "$1"
}

installed_version_for() {
  case "$1" in
    deb)
      /usr/bin/dpkg-query --show --showformat='${Version}' mantaray-vpn 2>/dev/null ||
        die 'mantaray-vpn is not installed through dpkg'
      ;;
    arch)
      installed_package_version="$(/usr/bin/pacman -Q mantaray-vpn-bin 2>/dev/null | /usr/bin/awk '{print $2}')" ||
        die 'mantaray-vpn-bin is not installed through pacman'
      printf '%s\n' "$installed_package_version" |
        /usr/bin/grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-1$' ||
        die 'installed Arch package version is not a supported release version'
      printf '%s\n' "${installed_package_version%-1}"
      ;;
    *) die 'unsupported Linux package format' ;;
  esac
}

install_package() {
  package_format="$1"
  package="$2"
  rollback_mode="$3"
  case "$package_format" in
    deb)
      if [ "$rollback_mode" = true ]; then
        run_apt --yes --reinstall --allow-downgrades --no-install-recommends install "$package"
      else
        run_apt --yes --only-upgrade --no-install-recommends install "$package"
      fi
      ;;
    arch) run_pacman "$package" ;;
    *) die 'unsupported Linux package format' ;;
  esac
}

if [ "${1:-}" = '--validate-manifest' ]; then
  [ "$#" -eq 2 ] || [ "$#" -eq 3 ] ||
    die 'usage: --validate-manifest MANIFEST [deb|arch]'
  validation_format="${3:-deb}"
  case "$validation_format" in deb|arch) ;; *) die 'unsupported Linux package format' ;; esac
  validation_dir="$(/usr/bin/mktemp -d)"
  trap '/bin/rm -rf "$validation_dir"' EXIT HUP INT TERM
  validate_manifest "$2" "$validation_dir/fields" "$validation_format"
  printf '%s\t%s\t%s\t%s\n' \
    "$(/bin/cat "$validation_dir/fields/version")" \
    "$(/bin/cat "$validation_dir/fields/version_code")" \
    "$(/bin/cat "$validation_dir/fields/sha256")" \
    "$(/bin/cat "$validation_dir/fields/url")"
  exit 0
fi

[ "${1:-}" = '--install' ] && [ "$#" -eq 2 ] ||
  die 'usage: --install DOWNLOADED_PACKAGE'
[ "$(/usr/bin/id -u)" -eq 0 ] || die 'installation requires root privileges'
[ -x /usr/bin/python3 ] || die 'python3 is unavailable'
[ -x /usr/bin/curl ] || die 'curl is unavailable'

package_format="$(detect_package_format)"
case "$package_format" in
  deb)
    [ -x /usr/bin/apt-get ] || die 'apt-get is unavailable'
    [ -x /usr/bin/dpkg-deb ] || die 'dpkg-deb is unavailable'
    [ -x /usr/bin/dpkg-query ] || die 'dpkg-query is unavailable'
    ;;
  arch)
    [ -x /usr/bin/pacman ] || die 'pacman is unavailable'
    [ -x /usr/bin/bsdtar ] || die 'bsdtar is unavailable'
    ;;
esac

source_package="$2"
case "$source_package" in /*) ;; *) die 'downloaded package path must be absolute' ;; esac
[ -f "$source_package" ] && [ ! -L "$source_package" ] ||
  die 'downloaded package must be a regular non-symlink file'

exec 9>/run/lock/mantaray-vpn-update.lock
/usr/bin/flock --exclusive --nonblock 9 || die 'another MantaRay update is running'

work_dir="$(/usr/bin/mktemp -d /var/tmp/mantaray-update.XXXXXXXX)"
recovery_dir='/var/lib/mantaray-vpn/update-recovery'
trap '/bin/rm -rf "$work_dir"' EXIT HUP INT TERM

channel_url="$(channel_url_for "$package_format")"
manifest="$work_dir/update.json"
fetch_https "$channel_url" "$manifest" || die 'cannot download update manifest'
validate_manifest "$manifest" "$work_dir/fields" "$package_format" ||
  die 'invalid update manifest'

target_version="$(/bin/cat "$work_dir/fields/version")"
target_sha256="$(/bin/cat "$work_dir/fields/sha256")"
target_url="$(/bin/cat "$work_dir/fields/url")"
expected_target_url="$(package_url_for "$package_format" "$target_version")"
[ "$target_url" = "$expected_target_url" ] ||
  die 'manifest package URL is outside the release namespace'

target_name="$(package_name_for "$package_format" "$target_version")"
candidate="$work_dir/$target_name"
/bin/cp -- "$source_package" "$candidate"
/bin/chmod 0600 "$candidate"
candidate_size="$(/usr/bin/stat --format='%s' "$candidate")"
[ "$candidate_size" -gt 0 ] && [ "$candidate_size" -le "$max_package_bytes" ] ||
  die 'candidate package size is invalid'
actual_sha256="$(/usr/bin/sha256sum "$candidate" | /usr/bin/awk '{print $1}')"
[ "$actual_sha256" = "$target_sha256" ] ||
  die 'candidate package SHA-256 does not match the live manifest'
validate_package_identity "$package_format" "$candidate" "$target_version"

installed_version="$(installed_version_for "$package_format")"
printf '%s\n' "$installed_version" |
  /usr/bin/grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' ||
  die 'installed package version is not a supported release version'
version_is_newer "$target_version" "$installed_version" ||
  die 'live channel does not contain a newer package'

# Download and verify the exact currently installed immutable package before
# mutating package-manager state. It is retained only long enough to provide
# an automatic rollback if install or post-install health validation fails.
rollback_name="$(package_name_for "$package_format" "$installed_version")"
rollback_url="$(package_url_for "$package_format" "$installed_version")"
rollback_checksum="$work_dir/${rollback_name}.sha256"
rollback_package="$work_dir/$rollback_name"
fetch_https "${rollback_url}.sha256" "$rollback_checksum" ||
  die 'cannot download rollback checksum'
rollback_sha256="$(/usr/bin/awk -v wanted="$rollback_name" '
  $2 == wanted { print tolower($1) }
' "$rollback_checksum")"
printf '%s\n' "$rollback_sha256" | /usr/bin/grep -Eq '^[0-9a-f]{64}$' ||
  die 'rollback checksum is invalid'
fetch_https "$rollback_url" "$rollback_package" ||
  die 'cannot download rollback package'
[ "$(/usr/bin/sha256sum "$rollback_package" | /usr/bin/awk '{print $1}')" = "$rollback_sha256" ] ||
  die 'rollback package SHA-256 mismatch'
validate_package_identity "$package_format" "$rollback_package" "$installed_version"

rollback() {
  reason="$1"
  log "$reason; restoring MantaRay $installed_version"
  if install_package "$package_format" "$rollback_package" true; then
    restored_version="$(installed_version_for "$package_format" 2>/dev/null || true)"
    if [ "$restored_version" = "$installed_version" ] && \
      /usr/bin/timeout --foreground 30 /usr/bin/systemctl \
        is-active --quiet com.mantaray.vpn.service
    then
      log "rollback to MantaRay $installed_version completed"
      return 0
    fi
    log 'rollback package installed but failed post-rollback validation'
  fi

  /bin/mkdir -p "$recovery_dir"
  /bin/chmod 0700 "$recovery_dir"
  /bin/cp -- "$rollback_package" "$recovery_dir/$rollback_name"
  /bin/chmod 0600 "$recovery_dir/$rollback_name"
  log "automatic rollback failed; recovery package preserved at $recovery_dir/$rollback_name"
  return 1
}

log "installing MantaRay $target_version over $installed_version ($package_format)"
if ! install_package "$package_format" "$candidate" false; then
  rollback 'package installation failed' || true
  die 'package installation failed; rollback attempted'
fi

if ! /usr/bin/timeout --foreground 30 /opt/mantaray/mantaray --update-healthcheck; then
  rollback 'new application healthcheck failed' || true
  die 'new application failed its healthcheck; rollback attempted'
fi
if ! /usr/bin/timeout --foreground 30 /usr/bin/systemctl \
  is-active --quiet com.mantaray.vpn.service
then
  rollback 'new Core service is not active' || true
  die 'new Core service failed its healthcheck; rollback attempted'
fi

log "MantaRay $target_version installed and healthchecked successfully"
printf 'installed %s\n' "$target_version"
