#!/bin/sh
#
# Copyright (C) 2026  Etersoft
# Copyright (C) 2026  Vitaly Lipatov <lav@etersoft.ru>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

# ALT kernel management: update-kernel and remove-old-kernels logic for apt and dnf
# Based on update-kernel and remove-old-kernels from update-kernel package (ALT Linux Team)

load_helper epm-sh-altlinux

# Kernel package versions are used without epoch and disttag: 6.18.54-alt1


#############################################################################
# Package manager backend (apt-rpm or dnf)

__ak_is_dnf()
{
    [ "$PMTYPE" = "dnf-rpm" ] || [ "$PMTYPE" = "dnf5-rpm" ]
}

# apt version string 6.18.54-alt1:sisyphus+434294.100.1.1@1790368635 -> 6.18.54-alt1 1790368635
__ak_apt_parse_version()
{
    local v="$1" bt=''
    case "$v" in
        *@*)
            bt="${v##*@}"
            v="${v%@*}"
            ;;
    esac
    # strip epoch and disttag
    echo "$v" | grep -q -E '^[0-9]+:' && v="${v#*:}"
    v="${v%%:*}"
    echo "$v $bt"
}

# print apt ids (name#version) for available in repo versions of the package
__ak_apt_repo_ids()
{
    local id
    a='' apt-cache $__EPM_APT_REPO_OPTIONS pkgnames "$1#" 2>/dev/null | while read -r id ; do
        a='' apt-cache $__EPM_APT_REPO_OPTIONS policy "$id" 2>/dev/null | grep -q -w 'pkglist$' && echo "$id"
    done
}

# print "version buildtime" for all versions of the package available in repo
__ak_repo_versions()
{
    if __ak_is_dnf ; then
        a='' $DNFCMD -q repoquery --qf '%{version}-%{release} %{buildtime}\n' "$1" 2>/dev/null
        return
    fi
    local id
    for id in $(__ak_apt_repo_ids "$1") ; do
        __ak_apt_parse_version "${id#*#}"
    done
}

# print "name version buildtime" for all kernel images available in repo
__ak_repo_kernels()
{
    if __ak_is_dnf ; then
        a='' $DNFCMD -q repoquery --qf '%{name} %{version}-%{release} %{buildtime}\n' 'kernel-image-*' 2>/dev/null | grep -v -- "-debuginfo "
        return
    fi
    local name id
    for name in $(a='' apt-cache $__EPM_APT_REPO_OPTIONS pkgnames 'kernel-image-' 2>/dev/null | grep '#' | grep -v -- '-debuginfo#' | sed -e 's|#.*||' | sort -u) ; do
        for id in $(__ak_apt_repo_ids "$name") ; do
            echo "$name $(__ak_apt_parse_version "${id#*#}")"
        done
    done
}

# print "name version" of external modules in repo built for the kernel
# args: flavour version
__ak_repo_modules()
{
    local flavour="$1" version="$2"
    if __ak_is_dnf ; then
        a='' $DNFCMD -q repoquery --qf '%{name} %{version}-%{release}\n' --whatrequires "kernel-image-$flavour-$version" 2>/dev/null | grep "^kernel-modules-.*-$flavour " | sort -u
        return
    fi
    local id
    id="$(__ak_apt_repo_ids "kernel-image-$flavour" | while read -r id ; do
        [ "$(__ak_apt_parse_version "${id#*#}" | cut -d' ' -f1)" = "$version" ] && echo "$id"
    done | head -n1)"
    [ -n "$id" ] || return 0
    a='' apt-cache $__EPM_APT_REPO_OPTIONS whatdepends "$id" 2>/dev/null | grep "kernel-modules-.*-$flavour#" | \
        sed -e 's|-[^-]*-[^-]*$||' -e 's|^ *||' | sort -u | while read -r id ; do
            echo "${id%%#*} $(__ak_apt_parse_version "${id#*#}" | cut -d' ' -f1)"
    done
}

# install packages: name or name=version
# kernel packages are installed alongside the old ones (Allow-Duplicated in apt, installonly in dnf)
__ak_install()
{
    local yes=''
    [ -n "$non_interactive" ] || [ -n "$force" ] && yes='-y'
    if __ak_is_dnf ; then
        local i modules=''
        for i in "$@" ; do
            case "$i" in
                kernel-modules-*)
                    modules="$modules ${i%%=*}"
                    ;;
            esac
        done
        sudocmd_dnf $yes $(subst_option download_only --downloadonly) $(__alt_dnf_installonly_opt $modules) install $(echo "$*" | exp_with_rpm_version)
        return
    fi
    sudocmd apt-get $__EPM_APT_REPO_OPTIONS install -o APT::Install::Virtual=true $yes $reinstall $(subst_option dryrun --simulate) $(subst_option download_only -d) "$@"
}

# remove packages: name=version
__ak_remove()
{
    local yes=''
    [ -n "$non_interactive" ] || [ -n "$force" ] && yes='-y'
    if __ak_is_dnf ; then
        # dnf removes kernel modules for removed kernels as dependent packages
        sudocmd_dnf $yes remove $(echo "$*" | exp_with_rpm_version)
        return
    fi
    sudocmd apt-get $yes $(subst_option dryrun --simulate) remove "$@"
}


#############################################################################
# Common logic

__ak_uname_r()
{
    a='' uname -r
}

# 6.18.54-6.18-alt1 -> 6.18
__ak_flavour_from_release()
{
    local f="${1#*-}"
    echo "${f%-*}"
}

# 6.18.54-6.18-alt1 -> 6.18.54-alt1
__ak_version_from_release()
{
    local ver="${1%%-*}" rel="${1##*-}"
    echo "$ver-$rel"
}

# compare versions like rpmevrdtcmp from update-kernel: by EVR (rpmevrcmp), then by buildtime
# args: version1 buildtime1 version2 buildtime2
# prints -1, 0 or 1
__ak_evrdtcmp()
{
    local v
    v="$(a='' rpmevrcmp "$1" "$3")"
    if [ "$v" = "0" ] && [ -n "$2" ] && [ -n "$4" ] ; then
        [ "$2" -gt "$4" ] 2>/dev/null && v=1
        [ "$2" -lt "$4" ] 2>/dev/null && v=-1
    fi
    echo "$v"
}

# is version $1 <= version $2
__ak_version_le()
{
    [ "$(__ak_evrdtcmp "$1" '' "$2" '')" -le 0 ]
}

# print the max line from "version [buildtime [rest]]" lines
__ak_max_line()
{
    local max='' maxv='' maxbt='' line v bt rest
    while read -r v bt rest ; do
        [ -n "$v" ] || continue
        if [ -z "$maxv" ] || [ "$(__ak_evrdtcmp "$v" "$bt" "$maxv" "$maxbt")" -gt 0 ] ; then
            maxv="$v"
            maxbt="$bt"
            max="$(echo "$v $bt $rest" | sed -e 's| *$||')"
        fi
    done
    [ -n "$max" ] && echo "$max"
    return 0
}

# sort "version [buildtime [rest]]" lines by version and buildtime (descending, the newest first)
__ak_sort_versions_desc()
{
    local lines max
    lines="$(cat | grep -v '^ *$')"
    while [ -n "$lines" ] ; do
        max="$(echo "$lines" | __ak_max_line)"
        echo "$max"
        lines="$(echo "$lines" | awk -v m="$max" '!done && $0 == m { done = 1 ; next } { print }')"
    done
}

# print name=version if the version of the package is available in repo, else just name
# (dnf does not update installed non-installonly package by name)
__ak_pkg_version()
{
    if __ak_repo_versions "$1" | grep -q "^$2 " ; then
        echo "$1=$2"
    else
        echo "$1"
    fi
}

# print the latest "version buildtime" line
__ak_max_version()
{
    __ak_max_line
}

# print short names of installed external modules for the flavour (kernel-modules-<name>-<flavour>)
# if kernel version $2 is set, only modules built for this kernel (they require kernel-image-<flavour> = version)
__ak_installed_modules()
{
    a='' rpm -qa --qf '[%{NAME} %{REQUIRENAME} %{REQUIREVERSION}\n]' "kernel-modules-*-$1" 2>/dev/null | \
        awk -v k="kernel-image-$1" -v v="$2" '{ sub(/^[0-9]+:/, "", $3) } v == "" || ($2 == k && ($3 == v || index($3, v ":") == 1)) { print $1 }' | \
        sed -e "s|^kernel-modules-||" -e "s|-$1\$||" | sort -u
}

# print stable flavours (x.y, std-def, un-def) with versions >= $1 from repo, the newest first
# skip -rc kernels unless mainline is set
__ak_next_flavours()
{
    local cutoff="${1:-0.0}" mainline="$2"
    __ak_repo_kernels | sed -e 's|^kernel-image-||' | grep -E '^(std-def|un-def|[0-9]+\.[0-9]+) ' | \
        { if [ -n "$mainline" ] ; then cat ; else grep -v -E ' [^ ]*\brc[0-9]' ; fi ; } | \
        while read -r flavour version bt ; do
            __ak_version_le "$cutoff" "$version" && echo "$version $bt $flavour"
        done | __ak_sort_versions_desc | cut -d' ' -f3 | awk '!seen[$0]++'
}

# run x11setupdrv/x11presetdrv after video drivers update (like update-kernel does)
__ak_x11_setup_drv()
{
    [ -x /usr/bin/Xorg ] || return 0
    echo "$*" | grep -q -E "drm|fglrx|nvidia" || return 0
    [ -x /usr/bin/x11setupdrv ] && sudocmd /usr/bin/x11setupdrv
    if [ -x /usr/sbin/x11presetdrv ] ; then
        sudocmd /usr/sbin/x11presetdrv
    else
        message "You might need to run x11presetdrv: video drivers updated but /usr/sbin/x11presetdrv is missing"
    fi
    sudocmd ldconfig
}

# list available and installed kernels
# args: [flavour]
__ak_list_kernels()
{
    local kernel_flavour="$1"
    local uname_r rflv def now
    uname_r="$(__ak_uname_r)"
    rflv="${kernel_flavour:-$(__ak_flavour_from_release "$uname_r")}"
    def="$(readlink /boot/vmlinuz 2>/dev/null)"
    def="${def#vmlinuz-}"
    now="$(date +%s)"

    local repo installed
    repo="$(__ak_repo_kernels | grep "^kernel-image-${kernel_flavour}")"
    installed="$(a='' rpm -qa --qf '%{NAME} %{VERSION}-%{RELEASE} %{BUILDTIME}\n' "kernel-image-${kernel_flavour}*" 2>/dev/null | grep -v -- "-debuginfo ")"
    [ -n "$kernel_flavour" ] && installed="$(echo "$installed" | grep "^kernel-image-$kernel_flavour ")"

    message 'List of available kernels:'
    local files
    files="$(a='' rpm -qa --qf '[%{NAME}\t%{FILENAMES}\n]' 'kernel-*' 2>/dev/null)"

    local count=0 pkg version bt flv kver mark addons
    while read -r pkg version bt ; do
        [ -n "$pkg" ] || continue
        flv="${pkg#kernel-image-}"
        kver="${version%-*}-$flv-${version##*-}"
        mark="   "
        echo "$repo" | grep -q "^$pkg $version " && mark="  *"
        if [ -n "$bt" ] && [ "$bt" -gt 0 ] 2>/dev/null ; then
            printf "%s %7s %s" "$mark" "($(( (now - bt) / 86400 )) d)" "$pkg-$version"
        else
            printf "%s %7s %s" "$mark" "?" "$pkg-$version"
        fi
        if a='' rpm -q "$pkg-$version" >/dev/null 2>&1 ; then
            [ "$def" = "$kver" ] && printf ' [default]' || printf ' [installed]'
        elif [ "$mark" = "  *" ] && [ "$rflv" = "$flv" ] ; then
            printf ' <--- upgrade'
        fi
        [ "$uname_r" = "$kver" ] && printf ' RUNNING'
        addons="$(echo "$files" | grep -e "/lib/modules/$kver/" -e "	/usr/include/linux-${version%-*}-$flv\$" | cut -f1 | sort -u | \
            grep -v "^$pkg\$" | sed -e "s|^kernel-modules-||" -e "s|^kernel-||" -e "s|-$flv\$||" | xargs)"
        [ -n "$addons" ] && printf ' | %s' "$addons"
        echo
        count=$((count + 1))
    done <<EOF
$( { echo "$repo" ; echo "$installed" ; } | sort -u -k1,2 | awk '{ print $2, $3, $1 }' | __ak_sort_versions_desc | awk '{ print $3, $1, $2 }' | tac)
EOF
    if [ "$count" -eq 0 ] ; then
        message "Nothing found."
    else
        message "[*] Latest available in repo. (d) Package age in days."
    fi
}

# parse -r value, sets ak_release and maybe ak_flavour
# formats: alt1, 5.7.19-alt1, 5.7.19-std-def-alt1, [kernel-image-]std-def-5.7.19-alt1, kernel-image-std-def=5.7.19-alt1
__ak_parse_release()
{
    local r="$1" ver flv rel
    r="${r#kernel-image-}"
    if echo "$r" | grep -q -E '^[0-9]+\.[0-9]+[-=#][0-9:]+\.[0-9]+\.[0-9.]+-[^-]+$' ; then
        # package format with x.y flavour: 6.18-6.18.44-alt1
        flv="${r%%[-=#]*}"
        rel="${r#"$flv"[-=#]}"
        ak_release="${rel#*:}"
    elif echo "$r" | grep -q -E '^[0-9]+\.[0-9.]+-.*-[^-]+$' ; then
        # uname -r compatible format
        ver="${r%%-*}"
        rel="${r##*-}"
        flv="${r#"$ver"-}"
        flv="${flv%-"$rel"}"
        ak_release="$ver-$rel"
    elif echo "$r" | grep -q -E '^(kernel-image-)?.*[-=#][0-9:]+\.[0-9.]+-[^-]+$' ; then
        # package format
        ver="${r#kernel-image-}"
        flv="${ver%[-=#]*-*}"
        rel="${ver#"$flv"[-=#]}"
        ak_release="${rel#*:}"
    else
        ak_release="$r"
        return 0
    fi
    if [ -n "$ak_flavour" ] && [ "$ak_flavour" != "$flv" ] ; then
        fatal 'Kernel flavour from -t $ak_flavour does not match flavour from -r ($flv).'
    fi
    ak_flavour="$flv"
    ak_user_flavour="-r"
}

epm_alt_update_kernel_help()
{
    message 'epm update-kernel - install the latest kernel and external modules (the same as update-kernel)
Usage: epm update-kernel [options]

Options:
  -l, --list           list available kernels
  -a, --all            select all available kernel modules to install
  -i, --interactive    interactive modules selection (via fzf)
  -H, --headers        add kernel headers to install
  --debuginfo          add debuginfo package to install
  --fw, --firmware     add firmware packages to install
  -A modulename        include (add) external module (by a short name)
  -D modulename        exclude (del) external module from install
  -f, -y, --force      force kernel upgrade
  -t, --type           select desired kernel flavour (6.12, std-def, etc),
                       by default it is the same as the booted kernel,
                       latest selects the newest flavour, mainline also allows -rc kernels
  -r, --release        desired kernel release for the current or specified flavour
                       (alt1, 6.12.10-alt1, 6.12.10-6.12-alt1, kernel-image-6.12-6.12.10-alt1)
  -u, --update         update package database before
  -n, --dry-run        simulate install
  -d, --download-only  download packages, but do not install
  --no-kernel          exclude the kernel-image from install
  --set-default KERNEL make the installed kernel default for boot (flavour for its latest kernel,
                       6.12.10-6.12-alt1 or kernel-image-6.12-6.12.10-alt1)

Run epm remove-old-kernels to uninstall unused kernels.
'
}

# update kernel and external modules to the latest (or specified) version
# options are the same as for update-kernel
epm_alt_update_kernel()
{
    local ak_flavour='' ak_release='' ak_user_flavour=''
    local all='' list='' headers='' debuginfo='' firmware='' nokernel='' update=''
    local addmodules='' delmodules='' set_default='' opt
    local interactive_select=''
    while [ -n "$1" ] ; do
        opt="$1"
        case "$opt" in
            -t|--type)
                ak_flavour="$2"
                ak_user_flavour="-t"
                shift
                ;;
            -r|--release)
                local release_arg="$2"
                shift
                ;;
            -a|--all)
                all=1
                ;;
            -l|--list)
                list=1
                ;;
            -i|--interactive)
                interactive_select=1
                ;;
            -A|--add)
                addmodules="$addmodules $2"
                shift
                ;;
            -D|--del)
                delmodules="$delmodules $2"
                shift
                ;;
            -H|--headers)
                headers=1
                ;;
            --fw|--firmware)
                firmware=1
                ;;
            --debuginfo)
                debuginfo=1
                ;;
            --no-kernel)
                nokernel=1
                ;;
            --set-default)
                set_default="$2"
                [ -n "$set_default" ] || fatal 'Kernel is not specified for --set-default'
                shift
                ;;
            -u|--update)
                update=1
                ;;
            -h|--help)
                epm_alt_update_kernel_help
                return
                ;;
            -d|--download-only)
                download_only="--download-only"
                ;;
            -n|--dry-run)
                dryrun="--dry-run"
                ;;
            -f|-y|--force)
                force="--force"
                ;;
            --reinstall)
                reinstall="--reinstall"
                ;;
            -v|--verbose)
                verbose="--verbose"
                ;;
            *)
                fatal 'Unknown option $opt for update-kernel'
                ;;
        esac
        shift
    done
    [ -n "$release_arg" ] && __ak_parse_release "$release_arg"

    if [ -n "$set_default" ] ; then
        __ak_set_default_kernel "$set_default"
        return
    fi

    if [ -n "$update" ] ; then
        docmd epm update || return
    else
        epm --quiet repo status >/dev/null 2>&1 || warning "Your package database is not up to date. Run # epm update"
    fi

    local uname_r current_flavour
    uname_r="$(__ak_uname_r)"
    if [ -n "$UPDATE_KERNEL_SYS_FLAVOUR" ] ; then
        current_flavour="$UPDATE_KERNEL_SYS_FLAVOUR"
    else
        current_flavour="$(__ak_flavour_from_release "$uname_r")"
    fi

    # mainline is latest with -rc kernels
    local mainline=''
    if [ "$ak_flavour" = "mainline" ] ; then
        mainline=1
        ak_flavour=latest
    fi
    if [ "$ak_flavour" = "latest" ] ; then
        ak_flavour="$(__ak_next_flavours "" "$mainline" | head -n1)"
        [ -n "$ak_flavour" ] || fatal "Latest flavour not found."
        info 'Currently available latest flavour is $ak_flavour'
    fi

    if [ -n "$list" ] ; then
        __ak_list_kernels "$ak_flavour"
        return
    fi

    local current_kernel_package
    current_kernel_package="$(a='' rpm -qf --qf '%{NAME}-%{VERSION}-%{RELEASE}\n' "/boot/vmlinuz-$uname_r" 2>/dev/null)" || current_kernel_package=''
    if [ -n "$current_kernel_package" ] ; then
        info 'Running kernel: $current_kernel_package'
    else
        info 'Running kernel: $uname_r is not from package'
    fi

    local kernel_flavour="${ak_flavour:-$current_flavour}"

    # get available kernels, switch to the next flavour if the current one is not available
    local kernels
    kernels="$(__ak_repo_versions "kernel-image-$kernel_flavour")"
    if [ -z "$kernels" ] ; then
        info 'There are no available kernels with flavour $kernel_flavour'
        [ -z "$ak_user_flavour" ] || fatal 'Remove $ak_user_flavour to attempt an automatic upgrade.'
        # compare kernel versions only: 6.6.58-un-def-alt1 is newer than 6.6.58-alt1 for rpm (ALT bug 51945)
        local current_kver
        current_kver="$(__ak_version_from_release "$uname_r")"
        echo "$kernel_flavour" | grep -q -E '^[0-9]+\.[0-9]+$' && current_kver="$kernel_flavour"
        info 'Searching for a newer flavour (>= $current_kver) ...'
        kernel_flavour="$(__ak_next_flavours "$current_kver" | tail -n1)"
        [ -n "$kernel_flavour" ] || fatal "No newer flavours found. Use -t to select another flavour."
        info 'Upgrade to the next available flavour $kernel_flavour'
        kernels="$(__ak_repo_versions "kernel-image-$kernel_flavour")"
    fi

    if [ -n "$ak_release" ] ; then
        kernels="$(echo "$kernels" | while read -r version bt ; do
            [ "$version" = "$ak_release" ] || [ "${version#*-}" = "$ak_release" ] && echo "$version $bt"
        done)"
        [ -n "$kernels" ] || fatal 'There are no available $kernel_flavour kernels with package release $ak_release'
    fi

    local kmax kmaxver kmaxbt
    kmax="$(echo "$kernels" | __ak_max_version)"
    kmaxver="${kmax% *}"
    kmaxbt="${kmax#* }"
    [ -n "$kmaxver" ] || fatal "Requested kernel not found."

    if a='' rpm -q "kernel-image-$kernel_flavour-$kmaxver" >/dev/null 2>&1 ; then
        info 'Latest available kernel kernel-image-$kernel_flavour-$kmaxver is already installed on your system.'
    else
        info 'Latest available kernel is kernel-image-$kernel_flavour-$kmaxver'
    fi
    if [ -n "$kmaxbt" ] && [ "$kmaxbt" != "$kmaxver" ] ; then
        local mons=$(( ($(date +%s) - kmaxbt) / 2592000 ))
        [ "$mons" -gt 0 ] && warning 'Selected kernel is $mons months old.'
    fi

    local extra=''
    # add headers
    if [ -n "$headers" ] || a='' rpm -q kernel-headers-common >/dev/null 2>&1 ; then
        extra="$extra $(__ak_pkg_version "kernel-headers-$kernel_flavour" "$kmaxver")"
    fi
    # install kernel-headers-modules if there is DKMS because it may need it at once
    if [ -n "$headers" ] || a='' rpm -q "kernel-headers-modules-$current_flavour" >/dev/null 2>&1 || \
       a='' rpm -q "kernel-headers-modules-$kernel_flavour" >/dev/null 2>&1 || is_command dkms ; then
        extra="$extra $(__ak_pkg_version "kernel-headers-modules-$kernel_flavour" "$kmaxver")"
    fi
    if [ -n "$debuginfo" ] ; then
        if [ -n "$(__ak_repo_versions "kernel-image-$kernel_flavour-debuginfo" | grep "^$kmaxver ")" ] ; then
            extra="$extra kernel-image-$kernel_flavour-debuginfo=$kmaxver"
        else
            warning "Debuginfo package not found for the new kernel."
        fi
    fi
    if [ -n "$firmware" ] ; then
        extra="$extra $(a='' rpm -qa --qf '%{NAME}\n' 'firmware-*' VENDOR='ALT Linux Team' 2>/dev/null | xargs)"
        a='' rpm -q firmware-linux >/dev/null 2>&1 || extra="$extra firmware-linux"
    fi

    # available external modules for the target kernel ("name version")
    local allmodules allnames
    allmodules="$(__ak_repo_modules "$kernel_flavour" "$kmaxver")"
    allnames="$(echo "$allmodules" | cut -d' ' -f1 | sed -e "s|^kernel-modules-||" -e "s|-$kernel_flavour\$||" | grep -v -- "-debuginfo\$" | sort -u)"
    local num_modules
    num_modules="$(echo $allnames | wc -w)"
    info 'Kernel $kernel_flavour version $kmaxver has $num_modules external modules.'

    if [ -n "$delmodules" ] ; then
        local missed
        missed="$(estrlist exclude "$allnames" "$delmodules")"
        [ -n "$missed" ] && warning 'Modules requested for exclusion but not found:' $missed
        allnames="$(estrlist exclude "$delmodules" "$allnames")"
    fi

    # modules installed for the booted kernel, not for older kernels of the flavour (ALT bug 29408)
    local booted_modules
    booted_modules="$(__ak_installed_modules "$current_flavour" "$(__ak_version_from_release "$uname_r")")"

    # check if selected kernel has all external modules installed for the booted kernel
    local lost
    lost="$(estrlist exclude "$allnames" "$booted_modules")"
    lost="$(estrlist exclude "$delmodules" "$lost")"
    if [ -n "$lost" ] ; then
        warning 'Selected kernel does not have the following external module(s) which you have installed for your booted $current_flavour kernel:' $lost
        warning "Do not answer yes if these modules are important for your system."
    fi

    # select modules: all, user selected, installed for booted or target flavour
    local selected
    if [ -n "$all" ] ; then
        selected="$allnames"
    else
        # all modules of another flavour we switch to (we can't boot it to update)
        local flavour_modules=''
        [ "$kernel_flavour" = "$current_flavour" ] || flavour_modules="$(__ak_installed_modules "$kernel_flavour")"
        selected="$(estrlist union "$flavour_modules $booted_modules $addmodules")"
        if [ -n "$interactive_select" ] ; then
            assure_exists fzf
            local chosen
            chosen="$(echo "$allnames" | a='' fzf -m --prompt="Select modules (Tab to mark)> " --header="Kernel $kernel_flavour $kmaxver external modules")"
            selected="$(estrlist union "$selected $chosen")"
        fi
        local missed
        missed="$(estrlist exclude "$allnames" "$addmodules")"
        [ -n "$missed" ] && warning 'Modules requested for install but not found:' $missed
        selected="$(estrlist intersection "$selected" "$allnames")"
    fi

    local modules='' m mv
    for m in $selected ; do
        mv="$(echo "$allmodules" | grep "^kernel-modules-$m-$kernel_flavour " | cut -d' ' -f2 | __ak_max_line)"
        [ -n "$mv" ] && modules="$modules kernel-modules-$m-$kernel_flavour=$mv"
    done
    [ -n "$selected" ] && info 'The following extra modules will be installed:' $selected

    local kernel_to_install=''
    [ -n "$nokernel" ] || kernel_to_install="kernel-image-$kernel_flavour=$kmaxver"

    # do we need to work?
    local needwork="$force$reinstall" i
    if [ -z "$needwork" ] ; then
        for i in $kernel_to_install $modules $extra ; do
            a='' rpm -q "$(echo "$i" | exp_with_rpm_version)" >/dev/null 2>&1 || needwork=1
        done
    fi
    if [ -z "$needwork" ] ; then
        message "Everything is already installed, thus no upgrade is possible. Use -f to force install."
        return 0
    fi

    __ak_install $kernel_to_install $modules $extra || fatal 'Failed to install kernel $kernel_flavour-$kmaxver packages'
    [ -n "$dryrun" ] || [ -n "$download_only" ] && return 0
    [ -n "$kernel_to_install" ] && { __ak_check_initrd "kernel-image-$kernel_flavour-$kmaxver" || return ; }
    sync
    __ak_x11_setup_drv $selected
}

# the boot filetrigger (bootloader-utils) does nothing without /proc and /sys (chroot, image build)
__ak_initrd_trigger_works()
{
    [ -d /proc/sys ] && [ -d /sys/class ] && [ -x /sbin/installkernel ]
}

# initrd is created by rpm filetrigger (installkernel), its failure does not change apt/dnf exit code
# (ALT bug 61092, 31027): check initrd for the installed kernel is really created
# arg: kernel package name (kernel-image-FLAVOUR-VERSION-RELEASE)
# Honors EPM_BOOT_DIR for test isolation
__ak_check_initrd()
{
    local kpkg="$1" vmlinuz kr
    local bootdir="${EPM_BOOT_DIR:-/boot}"
    __ak_initrd_trigger_works || return 0
    vmlinuz="$(a='' rpm -ql "$kpkg" 2>/dev/null | grep -m1 '^/boot/vmlinuz-')" || return 0
    kr="${vmlinuz#/boot/vmlinuz-}"
    [ -s "$bootdir/initrd-$kr.img" ] && return 0
    warning 'initrd $bootdir/initrd-$kr.img for the new kernel $kpkg was not created (see make-initrd errors above).'
    warning 'Do not reboot: the boot menu entry for the new kernel has no initrd.'
    info 'Fix the errors and run # /sbin/installkernel $kr (or remove the kernel: # epm remove $kpkg)'
    return 1
}

# print installed kernels as "flavour version buildtime" lines
__ak_installed_kernels()
{
    a='' rpm -qa --qf '%{NAME} %{VERSION}-%{RELEASE} %{BUILDTIME}\n' 'kernel-image-*' 2>/dev/null | grep -v -- "-debuginfo " | sed -e 's|^kernel-image-||'
}

# print "flavour version" of the installed kernel, installed kernels are on stdin ("flavour version buildtime" lines)
# args: kernel (kernel-image-6.12-6.12.10-alt1[.x86_64], 6.12.10-6.12-alt1 or flavour) [release]
# a flavour selects its latest installed kernel, unless the second arg is release
__ak_resolve_installed_kernel()
{
    local k="$1" mode="$2" installed ver ak_flavour='' ak_release='' ak_user_flavour=''
    installed="$(cat)"
    if [ "$mode" != "release" ] && echo "$installed" | grep -q "^${k#kernel-image-} " ; then
        ak_flavour="${k#kernel-image-}"
        ver="$(echo "$installed" | grep "^$ak_flavour " | cut -d' ' -f2,3 | __ak_max_line | cut -d' ' -f1)"
        echo "$ak_flavour $ver"
        return
    fi
    # strip arch from rpm -q output
    k="$(echo "$k" | sed -E -e 's/\.(x86_64|i586|aarch64|armh|ppc64le|riscv64|loongarch64|e2k(v[0-9]+)?|noarch)$//')"
    __ak_parse_release "$k"
    [ -n "$ak_flavour" ] || fatal 'Kernel $k is not installed. Use an installed flavour or kernel-image-FLAVOUR-VERSION-RELEASE format.'
    echo "$installed" | grep -q "^$ak_flavour $ak_release\( \|\$\)" || fatal 'Kernel kernel-image-$ak_flavour-$ak_release is not installed.'
    echo "$ak_flavour $ak_release"
}

# make the installed kernel default: installkernel switches /boot/vmlinuz and /boot/initrd.img links
# (the first boot menu entry uses them) without rebuilding initrd (ALT bug 38888)
# Honors EPM_BOOT_DIR and EPM_INSTALLKERNEL for test isolation
__ak_set_default_kernel()
{
    local fr kpkg vmlinuz kr saved
    local bootdir="${EPM_BOOT_DIR:-/boot}" installkernel="${EPM_INSTALLKERNEL:-/sbin/installkernel}"
    fr="$(__ak_installed_kernels | __ak_resolve_installed_kernel "$1")" || return
    kpkg="kernel-image-${fr% *}-${fr#* }"
    vmlinuz="$(a='' rpm -ql "$kpkg" 2>/dev/null | grep -m1 '^/boot/vmlinuz-')" || fatal 'Can'"'"'t find kernel image in $kpkg package.'
    kr="${vmlinuz#/boot/vmlinuz-}"
    [ -x "$installkernel" ] || fatal 'There is no $installkernel, install bootloader-utils package.'
    sudocmd "$installkernel" --keep-initrd "$kr" || fatal 'Failed to set $kpkg as default kernel.'

    # GRUB_DEFAULT=saved: the remembered entry overrides the first one
    if [ -s "$bootdir/grub/grubenv" ] && is_command grub-editenv ; then
        saved="$(a='' grub-editenv "$bootdir/grub/grubenv" list 2>/dev/null | sed -n -e 's|^saved_entry=||p')"
        case "$saved" in
            ''|0|gnulinux-simple-*)
                ;;
            *)
                info 'Resetting saved GRUB entry $saved to boot the first menu entry'
                sudocmd grub-editenv "$bootdir/grub/grubenv" unset saved_entry
                ;;
        esac
    fi
    info 'Default kernel is $kpkg ($kr).'
}

# print kernel release (as uname -r) of the latest kernel with uptime >= 1 day except the booted one $1
__ak_backup_kernel_release()
{
    [ -f /var/log/wtmp ] || return
    a='' last -a reboot 2>/dev/null | awk -v cur="$1" '$ 10 ~ /+/ && $ 11 != cur {print $11; exit }'
}

epm_alt_remove_old_kernels_help()
{
    message 'epm remove-old-kernels - remove old kernels (the same as remove-old-kernels)
Usage: epm remove-old-kernels [options] [kernel...]

Options:
  -f, -y, --force   do not ask for removal confirmation
  -n, --dry-run     just simulate removal
  -t, --type        remove old kernels for the specified flavour (6.12, un-def, etc)
  -a, --all         remove old kernels for all flavours (keep latest for each one)
  -A, --purge       purge other flavours
  --no-backup       do not keep the backup kernel

Kernels can be listed as kernel-image-6.12-6.12.111-alt1 or 6.12.111-6.12-alt1 (uname -r format),
then only these kernels will be removed.

Note: currently booted and backup kernels will not be removed.
The backup kernel is the latest kernel with uptime of more than a day.
All numeric kernels (x.y) are considered to be a single flavour.
'
}

# remove old kernels: keep the booted kernel, the backup one (latest with uptime >= 1 day)
# and the latest kernel of the flavour, all numeric flavours (x.y) are considered as a single flavour
# options are the same as for remove-old-kernels
epm_alt_remove_old_kernels()
{
    local kernel_flavour='' all='' nobackup='' kernels='' opt
    while [ -n "$1" ] ; do
        opt="$1"
        case "$opt" in
            -t|--type)
                kernel_flavour="$2"
                shift
                ;;
            -a|--all)
                all=all
                ;;
            -A|--purge)
                all=purge
                ;;
            --no-backup)
                nobackup=1
                ;;
            -h|--help)
                epm_alt_remove_old_kernels_help
                return
                ;;
            -n|--dry-run)
                dryrun="--dry-run"
                ;;
            -f|-y|--force)
                force="--force"
                ;;
            -*)
                fatal 'Unknown option $opt for remove-old-kernels'
                ;;
            *)
                kernels="$kernels $opt"
                ;;
        esac
        shift
    done

    local uname_r current_kernel_package current_flavour
    uname_r="$(__ak_uname_r)"
    current_kernel_package="$(a='' rpm -qf --qf '%{NAME}-%{VERSION}-%{RELEASE}\n' "/lib/modules/$uname_r/kernel" 2>/dev/null)" || current_kernel_package=''
    current_flavour="$(__ak_flavour_from_release "$uname_r")"
    if [ -n "$current_kernel_package" ] ; then
        info 'Running kernel version: $uname_r'
    else
        info 'Running kernel version: $uname_r (package not found)'
    fi

    # installed kernels as "flavour version buildtime" lines
    local all_kernels
    all_kernels="$(__ak_installed_kernels)"

    # remove the listed kernels only (ALT bug 55852)
    if [ -n "$kernels" ] ; then
        local k fr package remove='' remove_items=''
        for k in $kernels ; do
            fr="$(echo "$all_kernels" | __ak_resolve_installed_kernel "$k" release)" || return
            package="kernel-image-${fr% *}-${fr#* }"
            [ "$package" = "$current_kernel_package" ] && fatal 'Kernel $package is currently booted, it can'"'"'t be removed.'
            remove="$remove $package"
            remove_items="$remove_items kernel-image-${fr% *}=${fr#* }"
        done
        message "Will be removing kernels:" $remove
        __ak_remove $remove_items
        return
    fi

    if [ -z "$kernel_flavour" ] && echo "$current_flavour" | grep -q -E '^[0-9]+\.[0-9]+$' ; then
        # booted into one of x.y flavours
        kernel_flavour=latest
    fi

    local flavours
    if [ -n "$all" ] ; then
        flavours="$(echo "$all_kernels" | cut -d' ' -f1 | sort -u)"
    elif [ "$kernel_flavour" = "latest" ] ; then
        flavours="$(echo "$all_kernels" | grep -v -E ' [^ ]*\brc[0-9]' | cut -d' ' -f1 | grep -E '^(std-def|un-def|[0-9]+\.[0-9]+)$' | sort -u)"
    elif [ -n "$kernel_flavour" ] ; then
        flavours="$kernel_flavour"
    else
        flavours="$current_flavour"
    fi

    local backup_package=''
    if [ -z "$nobackup" ] ; then
        local backup_release
        backup_release="$(__ak_backup_kernel_release "$uname_r")"
        if [ -n "$backup_release" ] ; then
            backup_package="$(a='' rpm -qf --qf '%{NAME}-%{VERSION}-%{RELEASE}\n' "/lib/modules/$backup_release/kernel" 2>/dev/null)" || backup_package=''
            [ -n "$backup_package" ] && info 'Previous kernel with uptime more than a day: $backup_package (backup)'
        fi
        # wtmp can be rotated or flooded by audit: keep the newest kernel except the booted one (ALT bug 53849)
        if [ -z "$backup_package" ] ; then
            backup_package="$(echo "$all_kernels" | while read -r f v bt ; do
                    [ -n "$f" ] && [ "kernel-image-$f-$v" != "$current_kernel_package" ] && echo "$v ${bt:-0} $f"
                done | __ak_max_line | awk '{print "kernel-image-" $3 "-" $1}')"
            [ -n "$backup_package" ] && warning 'Backup kernel is not determined, keep the previous kernel $backup_package.'
        fi
    fi

    # the highest numeric flavour is used for the newest kernel check
    local newest_stable_kernel='' newest_stable_flavour
    newest_stable_flavour="$(echo "$flavours" | grep -E '^[0-9]+\.[0-9]+$' | __ak_max_line)"
    [ -n "$newest_stable_flavour" ] && newest_stable_kernel="kernel-image-$newest_stable_flavour-$(echo "$all_kernels" | grep "^$newest_stable_flavour " | cut -d' ' -f2,3 | __ak_max_line | cut -d' ' -f1)"

    local flavour versions newest kernel package keep='' remove='' remove_items=''
    for flavour in $flavours ; do
        versions="$(echo "$all_kernels" | grep "^$flavour " | cut -d' ' -f2,3 | __ak_sort_versions_desc | tac | cut -d' ' -f1)"
        newest="kernel-image-$flavour-$(echo "$versions" | tail -n1)"
        echo "$flavour" | grep -q -E '^[0-9]+\.[0-9]+$' && newest="$newest_stable_kernel"
        local pkglisted=1
        [ "$all" = "purge" ] || [ -n "$(__ak_repo_versions "kernel-image-$flavour")" ] || pkglisted=''
        for kernel in $versions ; do
            package="kernel-image-$flavour-$kernel"
            if [ "$package" = "$current_kernel_package" ] || [ "$package" = "$backup_package" ] ; then
                keep="$keep $package"
            elif [ "$all" = "purge" ] && [ "$flavour" != "$kernel_flavour" ] ; then
                remove="$remove $package"
            elif [ "$all" != "purge" ] && [ -z "$pkglisted" ] ; then
                # EOL flavour
                remove="$remove $package"
            elif [ "$package" = "$newest" ] ; then
                keep="$keep $package"
            else
                remove="$remove $package"
            fi
            echo " $remove " | grep -q " $package " && remove_items="$remove_items kernel-image-$flavour=$kernel"
        done
    done

    [ -n "$keep" ] && message "Keeping kernels:" $keep
    if [ -z "$remove" ] ; then
        message "Nothing to remove."
        return 0
    fi
    message "Will be removing kernels:" $remove

    __ak_remove $remove_items
}
