#!/bin/sh
set -eu
export LC_ALL=C

state_directory="${SANEFISH_INSTALL_STATE:-/var/lib/sanefish-patches/install}"
curl_bin="${SANEFISH_CURL:-/usr/bin/curl}"
rpm_bin="${SANEFISH_RPM:-/bin/rpm}"
pkcon_bin="${SANEFISH_PKCON:-/usr/bin/pkcon}"
dbus_send_bin="${SANEFISH_DBUS_SEND:-/usr/bin/dbus-send}"
patch_registration_root="${SANEFISH_PATCH_REGISTRATION_ROOT:-/usr/share/sanefish/patches}"
patch_root="${SANEFISH_PATCH_ROOT:-/usr/share/patchmanager/patches}"
patch_helper_root="${SANEFISH_PATCH_HELPER_ROOT:-/usr/libexec/sanefish-patches}"
patch_transaction_state="${SANEFISH_PATCH_TRANSACTION_STATE:-/var/lib/sanefish-patches/transaction}"
transaction_bin="${SANEFISH_TRANSACTION:-${patch_helper_root}/transaction}"

request_url="${state_directory}/request-url"
request_packages="${state_directory}/request-packages"
request_title="${state_directory}/request-title"
status_state="${state_directory}/status-state"
status_progress="${state_directory}/status-progress"
status_message="${state_directory}/status-message"
status_title="${state_directory}/status-title"
download_part="${state_directory}/package.rpm.part"
download_rpm="${state_directory}/package.rpm"
download_headers="${state_directory}/download.headers"
install_log="${state_directory}/install.log"
maximum_bytes=536870912
finished=0
title="Package"
success_message=""
job_directory=""
job_title="Package"
transaction_owned=0
recovery_attempted=0
packagekit_running=0
curl_pid=""
monitor_pid=""
guard=sanefish_install_batch

write_value()
{
    path="$1"
    value="$2"
    temporary="${path}.tmp.$$"
    umask 077
    printf '%s\n' "${value}" > "${temporary}"
    mv -f -- "${temporary}" "${path}"
}

write_status()
{
    write_value "${status_progress}" "$2"
    write_value "${status_message}" "$3"
    write_value "${status_title}" "${job_title}"
    # State is written last so readers never see a new state with stale text.
    write_value "${status_state}" "$1"
}

fail()
{
    failure_message="$1"
    if [ "${transaction_owned}" -eq 1 ] && [ "${packagekit_running}" -eq 0 ]; then
        if ! recover_owned_transaction; then
            failure_message="${failure_message} Some previously enabled patches could not be restored. Recovery information was kept in the installation log."
        fi
    fi
    {
        printf '\nResult: failed\n'
        printf 'Error: %s\n' "${failure_message}"
    } >> "${install_log}" 2>/dev/null || true
    write_status failed -1 "${failure_message}"
    finished=1
    exit 1
}

recover_owned_transaction()
{
    [ "${recovery_attempted}" -eq 0 ] || return 1
    recovery_attempted=1
    write_status installing -1 "Restoring previously enabled patches…"
    # Never restore while PackageKit may still be changing files.
    if "${transaction_bin}" recover >> "${install_log}" 2>&1; then
        transaction_owned=0
        return 0
    fi
    append_patchmanager_diagnostics
    return 1
}

append_patchmanager_diagnostics()
{
    if [ -r "${patch_transaction_state}/phase" ]; then
        printf '\nPatchmanager transaction phase: ' >> "${install_log}"
        sed -n '1p' "${patch_transaction_state}/phase" >> "${install_log}" \
            2>/dev/null || true
    fi
    if [ -r "${patch_transaction_state}/patchmanager.log" ]; then
        {
            printf '\nPatchmanager transaction output:\n'
            cat "${patch_transaction_state}/patchmanager.log"
        } >> "${install_log}" 2>/dev/null || true
    fi
}

patchmanager_lists_patch()
{
    patch_id="$1"
    [ -x "${dbus_send_bin}" ] || return 1
    response="$(${dbus_send_bin} --system --print-reply \
        --reply-timeout=5000 \
        --dest=org.SfietKonstantin.patchmanager \
        /org/SfietKonstantin/patchmanager \
        org.SfietKonstantin.patchmanager.listPatches 2>> "${install_log}")" \
        || return 1
    printf '%s\n' "${response}" | grep -Fq "string \"${patch_id}\""
}

wait_for_patchmanager_patch()
{
    patch_id="$1"
    attempts=0
    while [ "${attempts}" -lt 20 ]; do
        if patchmanager_lists_patch "${patch_id}"; then
            return 0
        fi
        attempts=$((attempts + 1))
        sleep 0.5
    done
    return 1
}

patch_payload_is_complete()
{
    patch_id="$1"
    [ -r "${patch_root}/${patch_id}/patch.json" ] \
        && [ -r "${patch_root}/${patch_id}/unified_diff.patch" ]
}

verify_patch_registration()
{
    patch_id="$(printf '%s' "${package_name}" | tr '-' '_')"
    registration="${patch_registration_root}/${patch_id}.json"
    [ -r "${registration}" ] || return 0
    grep -Fq '/usr/share/patchmanager/patches/' "${registration}" || return 0

    printf '\nPatchmanager patch ID: %s\n' "${patch_id}" >> "${install_log}"
    # RPM scriptlets already staged every payload in the shared transaction.
    # Do not re-run a per-package selector and cycle the whole patch stack.
    if ! patch_payload_is_complete "${patch_id}"; then
        append_patchmanager_diagnostics
        fail "${title} was installed, but its active Patchmanager files are incomplete."
    fi
    if ! wait_for_patchmanager_patch "${patch_id}"; then
        append_patchmanager_diagnostics
        fail "${title} was installed, but it does not appear in Patchmanager."
    fi
    if [ -f "${patch_transaction_state}/active" ]; then
        append_patchmanager_diagnostics
        fail "${title} was installed, but Patchmanager could not finish refreshing its patch list."
    fi

    printf 'Patchmanager registration: verified\n' >> "${install_log}"
    success_message="${title} was successfully installed."
}

cleanup()
{
    if [ -n "${monitor_pid}" ]; then
        kill "${monitor_pid}" 2>/dev/null || true
        wait "${monitor_pid}" 2>/dev/null || true
    fi
    if [ -n "${curl_pid}" ]; then
        kill "${curl_pid}" 2>/dev/null || true
        wait "${curl_pid}" 2>/dev/null || true
    fi
    if [ "${finished}" -eq 0 ]; then
        if [ "${transaction_owned}" -eq 1 ] && [ "${packagekit_running}" -eq 0 ]; then
            recover_owned_transaction || true
        fi
        append_patchmanager_diagnostics
        printf '\nResult: interrupted\n' >> "${install_log}" 2>/dev/null || true
        write_status failed -1 "The installation was interrupted. Check the installation log before trying again."
    fi
    # An interrupted pkcon client does not necessarily stop the PackageKit
    # daemon. Keep its input files and recovery state in that case.
    if [ -n "${job_directory}" ] && [ "${packagekit_running}" -eq 0 ]; then
        rm -f -- "${job_directory}/"*
        rmdir "${job_directory}" || true
    fi
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

download_and_verify()
{
    umask 077
    {
        printf 'Package title: %s\n' "${title}"
        printf 'Expected package: %s\n\n' "${expected_packages}"
    } >> "${install_log}"

    case "${url}" in https://?*) ;; *) fail "The direct download URL is invalid." ;; esac
    case "${expected_packages}" in
        ''|,*|*,|*,,*|*[!A-Za-z0-9_.+,-]*)
            fail "The expected RPM package name is invalid."
            ;;
    esac
    [ -n "${title}" ] || fail "The package title is missing."

    remaining=$((download_deadline - $(date +%s)))
    [ "${remaining}" -gt 0 ] || fail "The package downloads took too long."
    [ "${remaining}" -le 900 ] || remaining=900
    # Leave space for status/recovery files even if a server streams a package
    # without Content-Length. Bound each download by the remaining batch cap.
    download_limit="$(df -Pk "${state_directory}" | awk 'END {
        available = $4 * 1024 - 16777216
        if (available > 536870912) available = 536870912
        printf "%.0f", available
    }')"
    [ "${download_limit}" -gt 0 ] || fail "There is not enough free space to download the selected packages."
    remaining_bytes=$((1073741824 - total_bytes))
    [ "${download_limit}" -le "${remaining_bytes}" ] || download_limit="${remaining_bytes}"
    [ "${download_limit}" -gt 0 ] || fail "The selection exceeds the 1 GB download limit. Install fewer packages together."
    write_status downloading "$(((index - 1) * 100 / count))" "Downloading ${title} (${index} of ${count})…"

    "${curl_bin}" --location --proto '=https' --proto-redir '=https' --fail --silent --show-error \
        --connect-timeout 20 --max-time "${remaining}" --max-filesize "${download_limit}" \
        --dump-header "${download_headers}" --output "${download_part}" \
        "${url}" 2>> "${install_log}" &
    curl_pid=$!

    (
        while kill -0 "${curl_pid}" 2>/dev/null; do
            downloaded=0
            total=0
            [ -f "${download_part}" ] \
                && downloaded="$(stat -c '%s' "${download_part}" 2>/dev/null || printf '0')"
            if [ -r "${download_headers}" ]; then
                total="$(awk '
                    tolower($1) == "content-length:" {
                        gsub(/\r/, "", $2)
                        if ($2 ~ /^[0-9]+$/) value = $2
                    }
                    END { print value + 0 }
                ' "${download_headers}")"
            fi
            if [ "${total}" -gt 0 ]; then
                part_progress=$((downloaded * 100 / total))
                [ "${part_progress}" -le 99 ] || part_progress=99
                progress=$(((100 * (index - 1) + part_progress) / count))
            else
                progress=-1
            fi
            write_status downloading "${progress}" "Downloading ${title} (${index} of ${count})…"
            sleep 0.5
        done
    ) &
    monitor_pid=$!

    download_result=0
    wait "${curl_pid}" || download_result=$?
    curl_pid=""
    kill "${monitor_pid}" 2>/dev/null || true
    wait "${monitor_pid}" 2>/dev/null || true
    monitor_pid=""
    if [ "${download_result}" -ne 0 ]; then
        download_error="$(awk 'NF { line = $0 } END { print line }' \
            "${install_log}" 2>/dev/null || true)"
        if [ -n "${download_error}" ]; then
            fail "The download failed: ${download_error}"
        fi
        fail "The download failed. Check the connection and direct URL."
    fi

    [ -f "${download_part}" ] || fail "The download did not create a package file."
    downloaded="$(stat -c '%s' "${download_part}")"
    [ "${downloaded}" -gt 0 ] && [ "${downloaded}" -le "${maximum_bytes}" ] \
        || fail "The downloaded package has an invalid size."
    total_bytes=$((total_bytes + downloaded))
    [ "${total_bytes}" -le 1073741824 ] \
        || fail "The selection exceeds the 1 GB download limit. Install fewer packages together."
    mv -f -- "${download_part}" "${download_rpm}"

    write_status verifying "$((index * 100 / count))" "Verifying ${title} (${index} of ${count})…"
    metadata="$(${rpm_bin} -qp --qf '%{NAME}\n%{ARCH}\n%{EPOCHNUM}:%{VERSION}-%{RELEASE}\n' "${download_rpm}" 2>> "${install_log}")" \
        || fail "The downloaded file is not a valid RPM package."
    package_name="$(printf '%s\n' "${metadata}" | sed -n '1p')"
    package_architecture="$(printf '%s\n' "${metadata}" | sed -n '2p')"
    package_version="$(printf '%s\n' "${metadata}" | sed -n '3p')"
    [ -n "${package_name}" ] && [ -n "${package_version}" ] \
        || fail "The RPM has incomplete metadata."
    if [ -f "${job_directory}/names" ] && grep -Fxq "${package_name}" "${job_directory}/names"; then
        fail "More than one RPM was selected for ${package_name}."
    fi
    printf '%s\n' "${package_name}" >> "${job_directory}/names"

    case ",${expected_packages}," in
        *",${package_name},"*) ;;
        *) fail "The downloaded RPM does not match ${title}." ;;
    esac

    device_architecture="$(${rpm_bin} --eval '%{_arch}')"
    case "${device_architecture}" in
        armv7l) device_architecture=armv7hl ;;
        i386|i586|i686) device_architecture=i486 ;;
    esac
    case "${package_architecture}" in
        noarch|"${device_architecture}") ;;
        *) fail "The RPM is for ${package_architecture}, not ${device_architecture}." ;;
    esac

    ${rpm_bin} -K --nosignature "${download_rpm}" >> "${install_log}" 2>&1 \
        || fail "The RPM integrity check failed."
    ${rpm_bin} -qlp "${download_rpm}" > "${job_directory}/${index}.files" 2>> "${install_log}" \
        || fail "The RPM file list could not be read."
    if grep -Eq '/unified_diff\.patch$' "${job_directory}/${index}.files"; then
        has_patches=1
    fi
    printf '%s\t%s\t%s\t%s\n' "${package_name}" "${package_version}" "${package_architecture}" "${title}" \
        >> "${job_directory}/verified"
}

umask 077
job_directory="$(mktemp -d "${state_directory}/batch.XXXXXX")"
job_title="$(sed -n '1p' "${request_title}")"
[ -n "${job_title}" ] || job_title="Selected packages"
printf 'Started: %s\nPackage title: %s\n' "$(date '+%Y-%m-%d %H:%M:%S %z')" "${job_title}" > "${install_log}"
if [ -s "${state_directory}/request-batch" ]; then
    cp -- "${state_directory}/request-batch" "${job_directory}/request"
else
    # A request may have been queued by the previous controller before upgrade.
    printf '%s\t%s\t%s\n' "$(sed -n '1p' "${request_url}")" \
        "$(sed -n '1p' "${request_packages}")" "${job_title}" > "${job_directory}/request"
fi
count="$(wc -l < "${job_directory}/request" | tr -d ' ')"
[ "${count}" -gt 0 ] && [ "${count}" -le 64 ] || fail "The selection is empty or too large."
printf 'Selected packages: %s\n' "${count}" >> "${install_log}"
index=0
total_bytes=0
has_patches=0
download_deadline=$(($(date +%s) + 1800))
tab="$(printf '\t')"
set --
while IFS="${tab}" read -r url expected_packages title; do
    index=$((index + 1))
    download_part="${job_directory}/${index}.rpm.part"
    download_rpm="${job_directory}/${index}.rpm"
    download_headers="${job_directory}/${index}.headers"
    printf '\nPackage %s of %s\n' "${index}" "${count}" >> "${install_log}"
    download_and_verify
    set -- "$@" "${download_rpm}"
done < "${job_directory}/request"
[ "$#" -eq "${count}" ] || fail "The package selection is incomplete."

if [ "${count}" -gt 1 ] && [ "${has_patches}" -eq 1 ]; then
    [ -x "${transaction_bin}" ] || fail "The shared patch installer is missing."
    write_status installing -1 "Preparing previously enabled patches…"
    "${transaction_bin}" prepare-exclusive "${guard}" >> "${install_log}" 2>&1 \
        || fail "The existing patches could not be prepared safely. See the installation log."
    transaction_owned=1
fi

write_status installing -1 "Installing ${job_title}…"
printf '\nInstalling all %s verified RPMs in one PackageKit transaction.\n' "${count}" >> "${install_log}"
packagekit_running=1
packagekit_result=0
${pkcon_bin} --noninteractive --allow-untrusted --allow-reinstall \
    install-local "$@" >> "${install_log}" 2>&1 || packagekit_result=$?
packagekit_running=0
[ "${packagekit_result}" -eq 0 ] \
    || fail "PackageKit could not install ${job_title}. Some packages may have been installed. See the installation log."

if [ "${transaction_owned}" -eq 1 ]; then
    for participant in "${patch_transaction_state}/participants/"*; do
        [ -f "${participant}" ] || continue
        [ "${participant##*/}" = "${guard}" ] \
            || fail "A package did not finish configuring its patch files."
    done
    write_status installing -1 "Restoring previously enabled patches…"
    recovery_attempted=1
    if ! "${transaction_bin}" complete "${guard}" >> "${install_log}" 2>&1; then
        append_patchmanager_diagnostics
        fail "The packages were installed, but some previously enabled patches could not be restored."
    fi
    transaction_owned=0
fi
write_status installing -1 "Checking the installed packages…"
while IFS="${tab}" read -r package_name package_version package_architecture title; do
    installed="$(${rpm_bin} -q --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}\t%{ARCH}\n' "${package_name}" 2>> "${install_log}")" \
        || fail "${title} was not found after installation."
    expected="$(printf '%s\t%s\t%s' "${package_name}" "${package_version}" "${package_architecture}")"
    printf '%s\n' "${installed}" | grep -Fxq "${expected}" \
        || fail "The installed version of ${title} does not match the download."
    printf 'Installed version verified: %s\n' "${expected}" >> "${install_log}"
    verify_patch_registration
done < "${job_directory}/verified"
[ ! -f "${patch_transaction_state}/active" ] \
    || fail "Patchmanager could not finish refreshing its patch list."

write_status succeeded 100 "${job_title} was successfully installed."
{
    printf '\nResult: succeeded\n'
    printf 'Completed: %s\n' "$(date '+%Y-%m-%d %H:%M:%S %z')"
} >> "${install_log}"
finished=1
exit 0
