#!/usr/bin/bash
set -euo pipefail

if [[ ${EUID} -ne 0 ]]; then
    echo "repogoon-update-channel-worker must run as root" >&2
    exit 1
fi

operation=${1:-}
status_directory=/run/repogoon-update
status_file=${status_directory}/update-status.json
current_package=
previous_version=
current_version=
rollback_command=
phase=initializing
version_response=

json_escape() {
    local value=$1
    value=${value//\\/\\\\}
    value=${value//\"/\\\"}
    value=${value//$'\n'/\\n}
    value=${value//$'\r'/\\r}
    value=${value//$'\t'/\\t}
    printf '%s' "$value"
}

write_update_status() {
    [[ $operation == apply ]] || return 0
    local state=$1
    local message=$2
    local temporary
    if [[ -L $status_directory ]]; then
        echo "refusing symlinked update status directory" >&2
        return 1
    fi
    /usr/bin/install -d -o root -g root -m 0755 "$status_directory"
    temporary=$(/usr/bin/mktemp "${status_file}.tmp.XXXXXX")
    /usr/bin/chmod 0644 "$temporary"
    printf '{"schemaVersion":1,"command":"update-apply","state":"%s","phase":"%s","message":"%s","package":"%s","previousVersion":"%s","currentVersion":"%s","rollbackCommand":"%s","updatedAt":"%s"}\n' \
        "$(json_escape "$state")" \
        "$(json_escape "$phase")" \
        "$(json_escape "$message")" \
        "$(json_escape "$current_package")" \
        "$(json_escape "$previous_version")" \
        "$(json_escape "$current_version")" \
        "$(json_escape "$rollback_command")" \
        "$(/usr/bin/date --utc +%Y-%m-%dT%H:%M:%SZ)" > "$temporary"
    /usr/bin/mv -f "$temporary" "$status_file"
}

fail_operation() {
    local code=$1
    local message=$2
    trap - ERR
    [[ -z $version_response ]] || /usr/bin/rm -f "$version_response"
    write_update_status failed "$message" || true
    echo "$message" >&2
    exit "$code"
}

unexpected_failure() {
    local code=$1
    local line=$2
    trap - ERR
    [[ -z $version_response ]] || /usr/bin/rm -f "$version_response"
    phase=unexpected-failure
    write_update_status failed "update worker failed unexpectedly near line $line" || true
    echo "update worker failed unexpectedly near line $line" >&2
    exit "$code"
}

interrupt_operation() {
    phase=interrupted
    fail_operation 130 "RepoGoon update was interrupted; inspect package and service state before retrying"
}

trap 'unexpected_failure $? $LINENO' ERR
trap interrupt_operation INT TERM

case "$operation" in
    stable)
        current_package=repogoon-preview
        target_package=repogoon
        ;;
    preview)
        current_package=repogoon
        target_package=repogoon-preview
        ;;
    apply)
        target_package=
        write_update_status running "Acquiring global update locks"
        ;;
    *)
        echo "usage: repogoon-update-channel-worker stable|preview|apply" >&2
        exit 2
        ;;
esac

exec 8>/run/repogoon-secrets.lock
if ! /usr/bin/flock -n 8; then
    fail_operation 3 "another secret or update operation is already running"
fi

exec 9>/run/repogoon-update-channel.lock
if ! /usr/bin/flock -n 9; then
    fail_operation 4 "another update operation is already running"
fi

stable_installed=false
preview_installed=false
/usr/bin/rpm -q repogoon >/dev/null 2>&1 && stable_installed=true
/usr/bin/rpm -q repogoon-preview >/dev/null 2>&1 && preview_installed=true

if [[ $stable_installed == true && $preview_installed == true ]]; then
    fail_operation 5 "both repogoon and repogoon-preview are installed"
fi
if [[ $stable_installed == false && $preview_installed == false ]]; then
    fail_operation 6 "no RepoGoon RPM package is installed"
fi

if [[ $operation == apply ]]; then
    if [[ $preview_installed == true ]]; then
        current_package=repogoon-preview
        alternate_package=repogoon
    else
        current_package=repogoon
        alternate_package=repogoon-preview
    fi
    previous_version=$(/usr/bin/rpm -q --queryformat '%{VERSION}-%{RELEASE}' "$current_package")
    rollback_command="/usr/bin/dnf downgrade ${current_package}-${previous_version} -y"

    phase=package-update
    write_update_status running "Updating the installed $current_package package in place"
    if ! /usr/bin/dnf upgrade --refresh "$current_package" -y; then
        fail_operation 20 "same-channel package update failed"
    fi

    phase=package-verification
    if ! /usr/bin/rpm -q "$current_package" >/dev/null 2>&1 || \
       /usr/bin/rpm -q "$alternate_package" >/dev/null 2>&1; then
        fail_operation 21 "post-update package identity does not preserve the installed channel"
    fi
    current_version=$(/usr/bin/rpm -q --queryformat '%{VERSION}-%{RELEASE}' "$current_package")
    if ! /usr/bin/rpm --compare-versions "$current_version" gt "$previous_version"; then
        fail_operation 22 "post-update package version did not advance beyond the pre-update version"
    fi

    phase=service-verification
    write_update_status running "Restarting and verifying repogoon.service"
    /usr/bin/systemctl daemon-reload
    if ! /usr/bin/systemctl restart repogoon.service || \
       ! /usr/bin/systemctl is-active --quiet repogoon.service; then
        fail_operation 23 "repogoon.service failed post-update verification"
    fi

    main_pid=$(/usr/bin/systemctl show --property MainPID --value repogoon.service)
    if [[ ! $main_pid =~ ^[1-9][0-9]*$ ]]; then
        fail_operation 24 "repogoon.service has no verifiable main process"
    fi

    phase=database-verification
    write_update_status running "Verifying database connectivity with the active service credentials"
    if ! /usr/bin/timeout --signal=TERM --kill-after=5s 30s \
        /usr/bin/nsenter --target "$main_pid" --mount -- \
        /usr/bin/runuser -u repogoon -- /usr/bin/env \
        CREDENTIALS_DIRECTORY=/run/credentials/repogoon.service \
        NODE_ENV=production \
        /usr/bin/node --import tsx \
        /usr/share/repogoon/server/scripts/database-health.ts; then
        fail_operation 25 "database connectivity failed post-update verification"
    fi

    phase=runtime-version-verification
    port=$(/usr/bin/rgoon-ctl show --json server.port)
    if [[ ! $port =~ ^[0-9]{1,5}$ ]] || (( port < 1 || port > 65535 )); then
        fail_operation 26 "configured service port is invalid"
    fi
    version_response=$(/usr/bin/mktemp "${status_directory}/version-response.XXXXXX")
    if ! /usr/bin/wget2 --quiet --continue --output-document="$version_response" \
        --connect-timeout=2 --timeout=3 --tries=3 --retry-connrefused \
        "http://127.0.0.1:${port}/api/version" || \
       ! /usr/bin/grep -Fq "\"version\":\"${current_version}\"" "$version_response"; then
        /usr/bin/rm -f "$version_response"
        fail_operation 27 "running RepoGoon version does not match the installed package"
    fi
    /usr/bin/rm -f "$version_response"

    phase=complete
    write_update_status succeeded "RepoGoon update and post-update verification completed"
    echo "updated $current_package from $previous_version to $current_version and verified RepoGoon"
    exit 0
fi

if /usr/bin/rpm -q "$target_package" >/dev/null 2>&1; then
    echo "already on the $operation channel"
    exit 0
fi

# Preserve the shared account identity and enablement state while transitioning
# from packages whose uninstall scriptlets may still remove/disable them.
repogoon_uid=$(/usr/bin/id -u repogoon)
repogoon_gid=$(/usr/bin/getent group repogoon | /usr/bin/cut -d: -f3)
service_was_enabled=false
/usr/bin/systemctl is-enabled --quiet repogoon.service && service_was_enabled=true

/usr/bin/dnf swap "$current_package" "$target_package" -y

if ! /usr/bin/getent group repogoon >/dev/null; then
    /usr/sbin/groupadd -r -g "$repogoon_gid" repogoon
fi
if ! /usr/bin/getent passwd repogoon >/dev/null; then
    /usr/sbin/useradd -r -u "$repogoon_uid" -g repogoon -d /var/lib/repogoon \
        -s /sbin/nologin -c "RepoGoon service account" repogoon
fi
/usr/bin/systemctl daemon-reload
if [[ $service_was_enabled == true ]]; then
    /usr/bin/systemctl enable repogoon.service
fi
/usr/bin/systemctl restart repogoon.service

echo "switched to the $operation channel and restarted RepoGoon"
