#!/bin/bash
# Author: Christophe Casalegno / Brain 0verride
# Contact: christophe.casalegno@scalarx.com
# sxupdate
# Version 1.3
#
# Copyright (c) 2020 Christophe Casalegno
# 
# This program is free software: you can redistribute it and/or modify
#
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>
#
# The license is available on this server here: 
# https://www.christophe-casalegno.com/licences/gpl-3.0.txt

set -euo pipefail

export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none
export NEEDRESTART_MODE="${NEEDRESTART_MODE:-a}"   # a=auto restart, l=list only
export UCF_FORCE_CONFFOLD=1
export UCF_FORCE_CONFFNEW=0
export LC_ALL=C
export LANG=C

APT_OPTS=(
    -y
    -o "Acquire::Retries=3"
    -o "DPkg::Lock::Timeout=60"
    -o "Dpkg::Options::=--force-confdef"
    -o "Dpkg::Options::=--force-confold"
)

APT_UPDATE_OPTS=(
    -o "Acquire::Retries=3"
    -o "DPkg::Lock::Timeout=60"
)

FORCE_REPAIR=0
for arg in "$@"
do
    if [[ "$arg" == "--force-repair" ]]
    then
        FORCE_REPAIR=1
    fi
done

function root_check()
{
    if [[ ${EUID} -ne 0 ]]
    then
        echo "Error: This script must be run as root." >&2
        exit 1
    fi
}

function log_init()
{
    local LOGDIR="/var/log/sxupdate"
    mkdir -p "$LOGDIR"
    chmod 0750 "$LOGDIR" 2>/dev/null || true

    local LOGTS
    LOGTS="$(date +"%Y-%m-%d-%H-%M-%S")"

    local LOGFILE
    LOGFILE="$LOGDIR/sxupdate-${LOGTS}.log"

    exec > >(tee -a "$LOGFILE") 2>&1

    echo "=== sxupdate start ==="
    echo "date: $(date -Is)"
    echo "host: $(hostname -f 2>/dev/null || hostname)"
    echo "argv: $0 ${*:-}"
    echo "needrestart: mode=${NEEDRESTART_MODE}"
    echo "force-repair: ${FORCE_REPAIR}"
    echo
}

function warn_distro()
{
    local CODENAME=""
    CODENAME="$(. /etc/os-release 2>/dev/null && echo "${VERSION_CODENAME:-}")" || true

    if [[ -n "$CODENAME" && "$CODENAME" != "bookworm" ]]
    then
        echo "Warn: Tested on Bookworm only. Detected: ${CODENAME}"
    fi
}

function lock_check()
{
    command -v flock >/dev/null 2>&1 || { echo "Error: flock not found." >&2; exit 1; }

    local LOCKDIR="/run/lock"
    mkdir -p "$LOCKDIR" 2>/dev/null || true

    local NAME
    NAME="$(basename "$0")"

    local LOCKFILE
    LOCKFILE="$LOCKDIR/${NAME}.lock"

    if [[ -L "$LOCKFILE" ]]
    then
        echo "Error: lockfile is a symlink ($LOCKFILE)" >&2
        exit 1
    fi

    if [[ -e "$LOCKFILE" && ! -f "$LOCKFILE" ]]
    then
        echo "Error: lockfile is not a regular file ($LOCKFILE)" >&2
        exit 1
    fi

    if [[ -e "$LOCKFILE" ]]
    then
        local OWNER_UID
        OWNER_UID="$(stat -c %u "$LOCKFILE" 2>/dev/null)" || { echo "Error: Cannot stat $LOCKFILE" >&2; exit 1; }
        if [[ "$OWNER_UID" != "0" ]]
        then
            echo "Error: lockfile owner is not root ($LOCKFILE, uid=$OWNER_UID)" >&2
            exit 1
        fi
    fi

    local OLDUMASK
    OLDUMASK="$(umask)"
    umask 077

    local LOCKFD
    exec {LOCKFD}<>"$LOCKFILE" || { echo "Error: Cannot open lock $LOCKFILE" >&2; umask "$OLDUMASK"; exit 1; }

    if ! flock -n "$LOCKFD"
    then
        local PID
        PID="$(cat "$LOCKFILE" 2>/dev/null)"
        echo "Error: $NAME is already running (PID ${PID:-unknown})." >&2
        umask "$OLDUMASK"
        exit 1
    fi

    if [[ -w "/proc/self/fd/$LOCKFD" ]]
    then
        : > "/proc/self/fd/$LOCKFD"
    else
        : > "$LOCKFILE"
    fi

    printf '%s\n' "$$" >&"$LOCKFD"
    umask "$OLDUMASK"
}

function configure_needrestart()
{
    if [[ -d /etc/needrestart/conf.d ]]
    then
        cat > /etc/needrestart/conf.d/sxupdate.conf <<EOF
# Managed by sxupdate
\$nrconf{restart} = '${NEEDRESTART_MODE}';
EOF
    fi
}

function try_install()
{
    if [[ $# -eq 0 ]]
    then
        return 0
    fi

    apt-get "${APT_OPTS[@]}" install --no-install-recommends "$@" >/dev/null 2>&1 || true
}

function safe_curl()
{
    # safe_curl <url> <outfile>
    local URL="$1"
    local OUT="$2"

    try_install ca-certificates curl >/dev/null 2>&1 || true
    curl -fsSLo "$OUT" \
        --connect-timeout 10 --max-time 90 \
        --retry 3 --retry-delay 2 --retry-all-errors \
        "$URL"
}

function apt_update_capture()
{
    apt-get "${APT_UPDATE_OPTS[@]}" update --allow-releaseinfo-change
}

function is_gpg_related_failure()
{
    local OUT="$1"
    echo "$OUT" | grep -qE 'NO_PUBKEY|EXPKEYSIG|KEYEXPIRED|BADSIG|GPG error|The following signatures couldn.t be verified'
}

function ensure_backup_once()
{
    local FILE="$1"
    if [[ -f "$FILE" && ! -f "${FILE}.bak" ]]
    then
        cp -a "$FILE" "${FILE}.bak" 2>/dev/null || true
    fi
}

function list_apt_files()
{
    # outputs files on stdout, one per line
    local f

    echo "/etc/apt/sources.list"

    shopt -s nullglob
    for f in /etc/apt/sources.list.d/*.list
    do
        echo "$f"
    done
    for f in /etc/apt/sources.list.d/*.sources
    do
        echo "$f"
    done
    shopt -u nullglob
}

function repo_present_any()
{
    # repo_present_any <regex>
    local RE="$1"
    local f

    while read -r f
    do
        [[ -f "$f" ]] || continue
        if grep -qsE "$RE" "$f" 2>/dev/null
        then
            return 0
        fi
    done < <(list_apt_files)

    return 1
}

function apt_line_normalize_signed_by()
{
    # apt_line_normalize_signed_by <file> <match_regex> <keyring_path> <default_line>
    local FILE="$1"
    local MATCH_RE="$2"
    local KEYRING="$3"
    local DEFAULT_LINE="$4"

    if [[ ! -f "$FILE" ]]
    then
        echo "$DEFAULT_LINE" > "$FILE"
        return 0
    fi

    ensure_backup_once "$FILE"

    local TMP
    TMP="$(mktemp "${FILE}.tmp.XXXXXX")"

    awk -v re="$MATCH_RE" -v key="$KEYRING" '
    function starts_with_opts(line) {
        return (line ~ /^deb[[:space:]]+\[/)
    }

    function trim(s) {
        gsub(/^[[:space:]]+/, "", s)
        gsub(/[[:space:]]+$/, "", s)
        return s
    }

    function strip_signed_by(opts) {
        gsub(/(^|[[:space:]])signed-by=[^[:space:]]+/, "", opts)
        gsub(/[[:space:]]+/, " ", opts)
        return trim(opts)
    }

    function rebuild_with_opts(rest, opts, key) {
        opts = strip_signed_by(opts)
        rest = trim(rest)
        if (opts == "") {
            return "deb [signed-by=" key "] " rest
        }
        return "deb [" opts " signed-by=" key "] " rest
    }

    function add_opts(line, key,   rest) {
        rest = line
        sub(/^deb[[:space:]]+/, "", rest)
        rest = trim(rest)
        return "deb [signed-by=" key "] " rest
    }

    {
        if ($0 ~ /^deb[[:space:]]/ && $0 ~ re) {
            if (starts_with_opts($0)) {
                ob = index($0, "[")
                cb = index($0, "]")
                if (ob > 0 && cb > ob) {
                    opts = substr($0, ob + 1, cb - ob - 1)
                    rest = substr($0, cb + 1)
                    sub(/^\][[:space:]]*/, "", rest)
                    print rebuild_with_opts(rest, opts, key)
                } else {
                    print add_opts($0, key)
                }
            } else {
                print add_opts($0, key)
            }
        } else {
            print $0
        }
    }' "$FILE" > "$TMP"

    if ! cmp -s "$FILE" "$TMP"
    then
        mv -f "$TMP" "$FILE"
    else
        rm -f "$TMP"
    fi
}

function deb822_sources_update_signed_by()
{
    # deb822_sources_update_signed_by <file> <match_regex> <keyring_path>
    local FILE="$1"
    local MATCH_RE="$2"
    local KEYRING="$3"

    [[ -f "$FILE" ]] || return 0

    if ! grep -qsE "^URIs:[[:space:]].*(${MATCH_RE})" "$FILE" 2>/dev/null
    then
        return 0
    fi

    ensure_backup_once "$FILE"

    local TMP
    TMP="$(mktemp "${FILE}.tmp.XXXXXX")"

    awk -v re="$MATCH_RE" -v key="$KEYRING" '
    function trim(s) {
        gsub(/^[[:space:]]+/, "", s)
        gsub(/[[:space:]]+$/, "", s)
        return s
    }

    BEGIN {
        in_stanza=0
        match=0
        has_sb=0
    }

    function stanza_reset() {
        in_stanza=0
        match=0
        has_sb=0
    }

    function stanza_maybe_emit_sb() {
        if (match == 1 && has_sb == 0) {
            print "Signed-By: " key
        }
    }

    {
        # stanza boundary = blank line
        if ($0 ~ /^[[:space:]]*$/) {
            stanza_maybe_emit_sb()
            print $0
            stanza_reset()
            next
        }

        in_stanza=1

        if ($0 ~ /^URIs:[[:space:]]/) {
            line=$0
            if (line ~ re) {
                match=1
            }
            print $0
            next
        }

        if ($0 ~ /^Signed-By:[[:space:]]/) {
            if (match == 1) {
                print "Signed-By: " key
            } else {
                print $0
            }
            has_sb=1
            next
        }

        print $0
    }

    END {
        stanza_maybe_emit_sb()
    }' "$FILE" > "$TMP"

    if ! cmp -s "$FILE" "$TMP"
    then
        mv -f "$TMP" "$FILE"
    else
        rm -f "$TMP"
    fi
}

function normalize_repo_signed_by_all()
{
    # normalize_repo_signed_by_all <match_regex_for_deb_line_or_uris> <keyring_path> <default_line> <default_file_or_empty>
    local MATCH_RE="$1"
    local KEYRING="$2"
    local DEFAULT_LINE="$3"
    local DEFAULT_FILE="${4:-}"

    local FOUND=0
    local F

    while read -r F
    do
        [[ -f "$F" ]] || continue

        if [[ "$F" == *.sources ]]
        then
            if grep -qsE "^URIs:[[:space:]].*(${MATCH_RE})" "$F" 2>/dev/null
            then
                FOUND=1
                deb822_sources_update_signed_by "$F" "$MATCH_RE" "$KEYRING"
            fi
            continue
        fi

        # sources.list and *.list
        if grep -qsE "^deb[[:space:]].*(${MATCH_RE})" "$F" 2>/dev/null
        then
            FOUND=1
            apt_line_normalize_signed_by "$F" "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE"
        fi
    done < <(list_apt_files)

    if [[ "$FOUND" -eq 0 && -n "$DEFAULT_FILE" ]]
    then
        apt_line_normalize_signed_by "$DEFAULT_FILE" "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE"
    fi
}

function repair_webmin()
{
	try_install ca-certificates curl gnupg >/dev/null 2>&1 || true
	local KEYRING="/usr/share/keyrings/webmin-jcameron.gpg"
	local LIST="/etc/apt/sources.list.d/webmin-stable.list"
	local MATCH_RE="download[.]webmin[.]com/download/(newkey/)?repository"
	curl -fsSL --connect-timeout 10 --max-time 90 --retry 3 --retry-delay 2 --retry-all-errors "https://download.webmin.com/jcameron-key.asc" | gpg --dearmor > "$KEYRING" 2>/dev/null || true
	if [[ ! -s "$KEYRING" ]]
	then
		echo "Key fetch failed for Webmin"
	fi
	if [[ -s "$KEYRING" ]]
	then
		chmod 0644 "$KEYRING" 2>/dev/null || true
		local DEFAULT_LINE
		DEFAULT_LINE="deb [signed-by=$KEYRING] https://download.webmin.com/download/repository sarge contrib"
		normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" "$LIST"
	fi
}

function repair_sury()
{
    local MATCH_RE="packages[.]sury[.]org/php|sury[.]org/php|/sury/|[[:space:]]sury[[:space:]]"

    if ! repo_present_any "sury|packages[.]sury[.]org/php"
    then
        return 0
    fi

    echo "Repair: Sury repo..."
    try_install ca-certificates curl gnupg lsb-release >/dev/null 2>&1 || true

    local DEB="/tmp/debsuryorg-archive-keyring.deb"
    safe_curl "https://packages.sury.org/debsuryorg-archive-keyring.deb" "$DEB" || true

    if [[ -s "$DEB" ]]
    then
        dpkg -i "$DEB" >/dev/null 2>&1 || apt-get "${APT_OPTS[@]}" -f install >/dev/null 2>&1 || true
    fi

    local KEYRING="/usr/share/keyrings/debsuryorg-archive-keyring.gpg"
    local CODENAME
    CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"

    local DEFAULT_LINE
    DEFAULT_LINE="deb [signed-by=${KEYRING}] https://packages.sury.org/php/ ${CODENAME} main"

    normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
}

function nodesource_detect_major()
{
    local MAJOR=""
    local F

    for F in /etc/apt/sources.list /etc/apt/sources.list.d/*.list
    do
        [[ -f "$F" ]] || continue
        MAJOR="$(grep -Eo 'node_[0-9]+\.x' "$F" 2>/dev/null | head -n1 | grep -Eo '[0-9]+' || true)"
        if [[ -n "$MAJOR" ]]
        then
            break
        fi
    done

    if [[ -z "$MAJOR" ]]
    then
        MAJOR="${SX_NODE_MAJOR:-24}"
    fi

    echo "$MAJOR"
}

function repair_nodesource()
{
    local MATCH_RE="deb[.]nodesource[.]com"

    if ! repo_present_any "deb[.]nodesource[.]com"
    then
        return 0
    fi

    echo "Repair: NodeSource repo..."
    try_install ca-certificates curl gnupg >/dev/null 2>&1 || true

    local KEYRING="/usr/share/keyrings/nodesource.gpg"
    curl -fsSL \
        --connect-timeout 10 --max-time 90 \
        --retry 3 --retry-delay 2 --retry-all-errors \
        "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \
        | gpg --dearmor > "$KEYRING" 2>/dev/null || true

    if [[ -s "$KEYRING" ]]
    then
        chmod 0644 "$KEYRING" 2>/dev/null || true
    fi

    local MAJOR
    MAJOR="$(nodesource_detect_major)"

    local ARCH
    ARCH="$(dpkg --print-architecture)"

    local DEFAULT_LINE
    DEFAULT_LINE="deb [arch=$ARCH signed-by=$KEYRING] https://deb.nodesource.com/node_${MAJOR}.x nodistro main"

    normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
}

function repair_yarn()
{
    local MATCH_RE="dl[.]yarnpkg[.]com/debian"

    if ! repo_present_any "dl[.]yarnpkg[.]com/debian"
    then
        return 0
    fi

    echo "Repair: Yarn repo..."
    try_install ca-certificates curl gnupg >/dev/null 2>&1 || true

    local KEYRING="/usr/share/keyrings/yarn.gpg"
    curl -fsSL \
        --connect-timeout 10 --max-time 90 \
        --retry 3 --retry-delay 2 --retry-all-errors \
        "https://dl.yarnpkg.com/debian/pubkey.gpg" \
        | gpg --dearmor > "$KEYRING" 2>/dev/null || true

    if [[ -s "$KEYRING" ]]
    then
        chmod 0644 "$KEYRING" 2>/dev/null || true
        local DEFAULT_LINE
        DEFAULT_LINE="deb [signed-by=$KEYRING] https://dl.yarnpkg.com/debian/ stable main"
        normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
    fi
}

function repair_redis()
{
    local MATCH_RE="packages[.]redis[.]io"

    if ! repo_present_any "packages\\.redis\\.io"
    then
        return 0
    fi

    echo "Repair: Redis repo..."
    try_install lsb-release ca-certificates curl gnupg >/dev/null 2>&1 || true

    local KEYRING="/usr/share/keyrings/redis-archive-keyring.gpg"
    curl -fsSL \
        --connect-timeout 10 --max-time 90 \
        --retry 3 --retry-delay 2 --retry-all-errors \
        "https://packages.redis.io/gpg" \
        | gpg --dearmor > "$KEYRING" 2>/dev/null || true

    if [[ -s "$KEYRING" ]]
    then
        chmod 0644 "$KEYRING" 2>/dev/null || true
        local CODENAME
        CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"
        local DEFAULT_LINE
        DEFAULT_LINE="deb [signed-by=$KEYRING] https://packages.redis.io/deb ${CODENAME} main"
        normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
    fi
}

function elastic_detect_major()
{
    local MAJOR=""
    local F

    for F in /etc/apt/sources.list /etc/apt/sources.list.d/*.list
    do
        [[ -f "$F" ]] || continue
        MAJOR="$(grep -Eo 'packages/[0-9]+\.x/apt' "$F" 2>/dev/null | head -n1 | grep -Eo '^[0-9]+' || true)"
        if [[ -n "$MAJOR" ]]
        then
            break
        fi
    done

    if [[ -z "$MAJOR" ]]
    then
        MAJOR="${SX_ELASTIC_MAJOR:-8}"
    fi

    echo "$MAJOR"
}

function repair_elastic()
{
    local MATCH_RE="artifacts[.]elastic[.]co"

    if ! repo_present_any "artifacts\\.elastic\\.co"
    then
        return 0
    fi

    echo "Repair: Elastic repo..."
    try_install ca-certificates curl gnupg >/dev/null 2>&1 || true

    local KEYRING="/usr/share/keyrings/elasticsearch-keyring.gpg"
    curl -fsSL \
        --connect-timeout 10 --max-time 90 \
        --retry 3 --retry-delay 2 --retry-all-errors \
        "https://artifacts.elastic.co/GPG-KEY-elasticsearch" \
        | gpg --dearmor > "$KEYRING" 2>/dev/null || true

    if [[ -s "$KEYRING" ]]
    then
        chmod 0644 "$KEYRING" 2>/dev/null || true
    fi

    local MAJOR
    MAJOR="$(elastic_detect_major)"

    local DEFAULT_LINE
    DEFAULT_LINE="deb [signed-by=$KEYRING] https://artifacts.elastic.co/packages/${MAJOR}.x/apt stable main"

    normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
}

function repair_docker()
{
    local MATCH_RE="download[.]docker[.]com/linux/debian"

    if ! repo_present_any "$MATCH_RE"
    then
        return 0
    fi

    echo "Repair: Docker repo..."
    try_install ca-certificates curl >/dev/null 2>&1 || true

    mkdir -p /etc/apt/keyrings 2>/dev/null || true
    safe_curl "https://download.docker.com/linux/debian/gpg" "/etc/apt/keyrings/docker.asc" || true
    chmod a+r /etc/apt/keyrings/docker.asc 2>/dev/null || true

    local CODENAME
    CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"

    local KEYRING="/etc/apt/keyrings/docker.asc"
    local DEFAULT_LINE
    DEFAULT_LINE="deb [signed-by=${KEYRING}] https://download.docker.com/linux/debian ${CODENAME} stable"

    normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
}

function repair_pgdg()
{
    local MATCH_RE="apt[.]postgresql[.]org"

    if ! repo_present_any "apt\\.postgresql\\.org"
    then
        return 0
    fi

    echo "Repair: PostgreSQL PGDG repo..."
    try_install ca-certificates curl >/dev/null 2>&1 || true

    install -d /usr/share/postgresql-common/pgdg 2>/dev/null || true
    safe_curl "https://www.postgresql.org/media/keys/ACCC4CF8.asc" "/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc" || true

    local KEYRING="/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc"
    local CODENAME
    CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"

    local DEFAULT_LINE
    DEFAULT_LINE="deb [signed-by=${KEYRING}] https://apt.postgresql.org/pub/repos/apt ${CODENAME}-pgdg main"

    normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
}

function mariadb_detect_series()
{
    local SERIES=""
    local F

    for F in /etc/apt/sources.list /etc/apt/sources.list.d/*.list
    do
        [[ -f "$F" ]] || continue
        SERIES="$(grep -Eo '/repo/[0-9]+\.[0-9]+' "$F" 2>/dev/null | head -n1 | sed 's#.*/repo/##' || true)"
        if [[ -n "$SERIES" ]]
        then
            break
        fi
    done

    if [[ -z "$SERIES" ]]
    then
        SERIES="${SX_MARIADB_SERIES:-11.8}"
    fi

    echo "$SERIES"
}

function repair_mariadb()
{
    local MATCH_RE="mariadb[.]com|dlm[.]mariadb[.]com|downloads[.]mariadb[.]com|r[.]mariadb[.]com|/MariaDB/|MariaDB"

    if ! repo_present_any "$MATCH_RE"
    then
        return 0
    fi

    echo "Repair: MariaDB repo..."
    try_install ca-certificates curl gnupg >/dev/null 2>&1 || true

    local KEYRING="/usr/share/keyrings/mariadb-archive-keyring.gpg"
    curl -fsSL \
        --connect-timeout 10 --max-time 90 \
        --retry 3 --retry-delay 2 --retry-all-errors \
        "https://mariadb.org/mariadb_release_signing_key.asc" \
        | gpg --dearmor > "$KEYRING" 2>/dev/null || true

    if [[ -s "$KEYRING" ]]
    then
        chmod 0644 "$KEYRING" 2>/dev/null || true
    fi

    local CODENAME
    CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"

    local SERIES
    SERIES="$(mariadb_detect_series)"

    local BASEURL
    BASEURL="${SX_MARIADB_BASEURL:-https://r.mariadb.com}"

    local DEFAULT_LINE
    DEFAULT_LINE="deb [signed-by=${KEYRING}] ${BASEURL}/repo/${SERIES}/debian ${CODENAME} main"

    normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
}

function repair_mysql()
{
    local MATCH_RE="repo[.]mysql[.]com"

    if ! repo_present_any "repo\\.mysql\\.com"
    then
        return 0
    fi

    echo "Repair: MySQL repo key (best effort)..."
    try_install ca-certificates curl gnupg >/dev/null 2>&1 || true

    local KEYRING="/usr/share/keyrings/mysql-repo.gpg"
    curl -fsSL \
        --connect-timeout 10 --max-time 90 \
        --retry 3 --retry-delay 2 --retry-all-errors \
        "https://repo.mysql.com/RPM-GPG-KEY-mysql" \
        | gpg --dearmor > "$KEYRING" 2>/dev/null || true

    if [[ -s "$KEYRING" ]]
    then
        chmod 0644 "$KEYRING" 2>/dev/null || true
    fi

    local CODENAME
    CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"

    local DEFAULT_LINE
    DEFAULT_LINE="deb [signed-by=${KEYRING}] https://repo.mysql.com/apt/debian/ ${CODENAME} mysql-8.4-lts"

    normalize_repo_signed_by_all "$MATCH_RE" "$KEYRING" "$DEFAULT_LINE" ""
}

function gpg_recv_key_any()
{
    # gpg_recv_key_any <keyid>
    local K="$1"

    if gpg --keyserver keyserver.ubuntu.com --recv-keys "$K" >/dev/null 2>&1
    then
        return 0
    fi

    if gpg --keyserver hkps://keys.openpgp.org --recv-keys "$K" >/dev/null 2>&1
    then
        return 0
    fi

    if gpg --keyserver hkp://pgp.mit.edu --recv-keys "$K" >/dev/null 2>&1
    then
        return 0
    fi

    return 1
}

function last_resort_keyserver_import()
{
    local OUT="$1"
    local KEYIDS=""

    KEYIDS="$(echo "$OUT" | awk '
        /NO_PUBKEY|EXPKEYSIG|KEYEXPIRED|BADSIG/ {
            for (i=1; i<=NF; i++) {
                if ($i ~ /^[0-9A-F]{8,40}$/) print $i
            }
        }' | sort -u)"

    if [[ -z "$KEYIDS" ]]
    then
        return 0
    fi

    echo "Last resort: keyserver import for failing key ids: $KEYIDS"
    try_install gnupg >/dev/null 2>&1 || true

    local K
    for K in $KEYIDS
    do
        echo "Importing key: $K"
        if gpg_recv_key_any "$K"
        then
            gpg --export "$K" | gpg --dearmor > "/etc/apt/trusted.gpg.d/auto-${K}.gpg" 2>/dev/null || true
        else
            echo "Warning: keyserver fetch failed for $K"
        fi
    done
}

function apt_update_autofix()
{
    echo "Running apt-get update (with auto-fix)..."

    if [[ "$FORCE_REPAIR" -eq 1 ]]
    then
        echo "Force repair enabled: running repairs before apt update..."
        repair_webmin
        repair_sury
        repair_nodesource
        repair_yarn
        repair_redis
        repair_elastic
        repair_docker
        repair_pgdg
        repair_mariadb
        repair_mysql
    fi

    local OUT=""
    OUT="$(apt_update_capture 2>&1)" || true
    echo "$OUT"

    if ! is_gpg_related_failure "$OUT"
    then
        return 0
    fi

    echo
    echo "APT update returned GPG/signature issues. Attempting non-destructive repairs..."

    repair_webmin
    repair_sury
    repair_nodesource
    repair_yarn
    repair_redis
    repair_elastic
    repair_docker
    repair_pgdg
    repair_mariadb
    repair_mysql

    OUT="$(apt_update_capture 2>&1)" || true
    echo "$OUT"

    if ! is_gpg_related_failure "$OUT"
    then
        return 0
    fi

    echo
    echo "APT update still failing. Applying last-resort keyserver import..."
    last_resort_keyserver_import "$OUT"

    echo "Final apt-get update attempt..."
    apt_update_capture
}


function update_webmin()
{
    echo "----------------------------------------------------"
    echo "Handling Webmin (priority update)..."

    apt-get "${APT_OPTS[@]}" -d install webmin
    apt-get "${APT_OPTS[@]}" install webmin

    echo "Webmin processed."
    echo "----------------------------------------------------"
}

function system_upgrade()
{
    echo "Pre-fetching system packages (full-upgrade)..."
    apt-get "${APT_OPTS[@]}" -d full-upgrade

    echo "Launching full system full-upgrade..."
    apt-get "${APT_OPTS[@]}" full-upgrade
}

function cleanup()
{
    if [[ "${1:-}" == "--clean" ]]
    then
        echo "Cleaning up unused packages..."
        apt-get -y autoremove -o "DPkg::Lock::Timeout=60" || true
        apt-get -y autoclean -o "DPkg::Lock::Timeout=60" || true
    fi
}

function check_reboot()
{
    if [[ -f /var/run/reboot-required ]]
    then
        echo "Reboot is required."
    fi
}

# --- Main ---
root_check
log_init "$@"
warn_distro
lock_check
configure_needrestart

apt_update_autofix
update_webmin
system_upgrade
cleanup "${1:-}"

echo "Upgrade sequence complete."
check_reboot
echo "=== sxupdate end ==="

