#!/bin/bash
set -Eeuo pipefail

STATE_DIR="${IBHLINKUA_UPDATE_STATE_DIR:-/var/lib/ibhlinkua/update}"
PENDING_PACKAGE="${STATE_DIR}/pending.deb"
ROLLBACK_DIR="${STATE_DIR}/rollback"
CONFIG_BACKUP="${ROLLBACK_DIR}/configuration.tar.gz"
STATUS_FILE="${STATE_DIR}/status"
LOG_FILE="${STATE_DIR}/update.log"
LOCK_FILE="${STATE_DIR}/update.lock"
SERVICE=ibhlinkua.service
PACKAGE=ibhlinkua
SIGNATURE_VERIFIER="${IBHLINKUA_UPDATE_SIGNATURE_VERIFIER:-/usr/lib/ibhlinkua/verify-deb-signature}"
HEALTH_TIMEOUT="${IBHLINKUA_UPDATE_HEALTH_TIMEOUT:-90}"
HEALTH_STABLE_SECONDS="${IBHLINKUA_UPDATE_HEALTH_STABLE_SECONDS:-15}"

case "${STATE_DIR}" in
    /*) ;;
    *) echo "Update state directory must be absolute" >&2; exit 2 ;;
esac
[ "${STATE_DIR}" != / ] && [ "${STATE_DIR}" != /var ] && [ "${STATE_DIR}" != /var/lib ] || {
    echo "Refusing unsafe update state directory: ${STATE_DIR}" >&2
    exit 2
}
case "${HEALTH_TIMEOUT}:${HEALTH_STABLE_SECONDS}" in
    *[!0-9:]*|0:*|*:0) echo "Health-check intervals must be positive integers" >&2; exit 2 ;;
esac

mkdir -p "${STATE_DIR}" "${ROLLBACK_DIR}"
chmod 0700 "${STATE_DIR}" "${ROLLBACK_DIR}"
exec 9>"${LOCK_FILE}"

write_status() {
    local state="$1"
    local message="$2"
    local temporary="${STATUS_FILE}.new"
    printf 'state=%s\nmessage=%s\ntime=%s\n' \
        "${state}" "${message//$'\n'/ }" "$(date --iso-8601=seconds)" > "${temporary}"
    chmod 0600 "${temporary}"
    mv -f "${temporary}" "${STATUS_FILE}"
}

log() {
    printf '%s %s\n' "$(date --iso-8601=seconds)" "$*" >> "${LOG_FILE}"
}

package_field() {
    dpkg-deb --field "$1" "$2" 2>/dev/null | tr -d '\r\n'
}

validate_package() {
    local file="$1"
    local name arch version installed_arch installed_version package_path
    local apt_validation_dir apt_validation_package

    [ -f "${file}" ] || { log "Update package is missing: ${file}"; return 1; }
    [ ! -L "${file}" ] || { log "Symbolic links are not accepted as update packages"; return 1; }
    [ -x "${SIGNATURE_VERIFIER}" ] || { log "Manufacturer signature verifier is unavailable"; return 1; }
    "${SIGNATURE_VERIFIER}" "${file}" >>"${LOG_FILE}" 2>&1 || {
        log "Manufacturer signature verification failed"
        return 1
    }
    dpkg-deb --info "${file}" >/dev/null 2>&1 || { log "Uploaded file is not a Debian package"; return 1; }

    name="$(package_field "${file}" Package)"
    arch="$(package_field "${file}" Architecture)"
    version="$(package_field "${file}" Version)"
    installed_arch="$(dpkg --print-architecture)"
    installed_version="$(dpkg-query -W -f='${Version}' "${PACKAGE}" 2>/dev/null || true)"

    [ "${name}" = "${PACKAGE}" ] || { log "Unexpected package name: ${name}"; return 1; }
    { [ "${arch}" = "${installed_arch}" ] || [ "${arch}" = all ]; } || {
        log "Package architecture ${arch} does not match ${installed_arch}"
        return 1
    }
    [ -n "${version}" ] || { log "Package has no version"; return 1; }
    if [ -n "${installed_version}" ] && ! dpkg --compare-versions "${version}" gt "${installed_version}"; then
        log "Version ${version} is not newer than installed version ${installed_version}"
        return 1
    fi

    if ! dpkg-deb --fsys-tarfile "${file}" | tar -tf - | (
        invalid_path=0
        while IFS= read -r entry; do
            case "${entry}" in
                ./opt/IBHLinkUA/*|./etc/ibhlinkua/*|./etc/wt/*|./etc/init.d/IBHLinkUA|./lib/systemd/system/ibhlinkua*.service|./usr/local/lib/*|./usr/lib/ibhlinkua/*|./usr/share/ibhlinkua/update-signing/*)
                    ;;
                ./|./opt/|./etc/|./etc/init.d/|./lib/|./lib/systemd/|./lib/systemd/system/|./usr/|./usr/local/|./usr/local/lib/|./usr/lib/|./usr/lib/ibhlinkua/|./usr/share/|./usr/share/ibhlinkua/|./usr/share/ibhlinkua/update-signing/)
                    ;;
                *)
                    if [ "${invalid_path}" -eq 0 ]; then
                        log "Package contains an unexpected path: ${entry}"
                    fi
                    invalid_path=1
                    ;;
            esac
        done
        [ "${invalid_path}" -eq 0 ]
    ); then
        log "Package payload validation failed"
        return 1
    fi

    package_path="$(readlink -f "${file}")"
    # Wt stores uploads under an extensionless spool filename. APT accepts a
    # local Debian package only when the command-line path ends in .deb, so use
    # a private temporary name for the dependency simulation. Prefer a hard
    # link to avoid copying a potentially large package.
    apt_validation_dir="$(mktemp -d /tmp/ibhlinkua-apt.XXXXXX)"
    apt_validation_package="${apt_validation_dir}/update.deb"
    if ! ln "${package_path}" "${apt_validation_package}" 2>/dev/null; then
        cp "${package_path}" "${apt_validation_package}"
    fi
    if ! DEBIAN_FRONTEND=noninteractive apt-get --simulate \
        -o Debug::NoLocking=1 --no-install-recommends install "${apt_validation_package}" \
        >>"${LOG_FILE}" 2>&1; then
        rm -rf "${apt_validation_dir}"
        log "Package dependencies are not satisfiable on this system"
        return 1
    fi
    rm -rf "${apt_validation_dir}"

    log "Validated ${name} ${version} for ${arch}"
}

stage_package() {
    local source="$1"
    local temporary="${PENDING_PACKAGE}.new"

    flock -n 9 || { log "Another update operation is active"; exit 1; }
    validate_package "${source}" || { write_status validation_failed "Das Update-Paket ist nicht gueltig signiert oder mit diesem System nicht kompatibel."; exit 1; }
    install -m 0600 "${source}" "${temporary}"
    mv -f "${temporary}" "${PENDING_PACKAGE}"
    sync -f "${PENDING_PACKAGE}" 2>/dev/null || sync
    write_status staged "Das Update wurde geprueft und ist installationsbereit."
}

backup_installed_version() {
    local old_package

    rm -rf "${ROLLBACK_DIR}"
    mkdir -p "${ROLLBACK_DIR}"
    chmod 0700 "${ROLLBACK_DIR}"

    log "Creating rollback package from the installed version"
    (cd "${ROLLBACK_DIR}" && dpkg-repack "${PACKAGE}") >>"${LOG_FILE}" 2>&1
    old_package="$(find "${ROLLBACK_DIR}" -maxdepth 1 -type f -name 'ibhlinkua_*.deb' -print -quit)"
    [ -n "${old_package}" ] || { log "dpkg-repack did not create a rollback package"; return 1; }

    tar -C / -czf "${CONFIG_BACKUP}" \
        --ignore-failed-read etc/ibhlinkua etc/wt >>"${LOG_FILE}" 2>&1
    sha256sum "${old_package}" > "${ROLLBACK_DIR}/package.sha256"
    printf '%s\n' "${old_package}" > "${ROLLBACK_DIR}/package.path"
    sync -f "${old_package}" 2>/dev/null || sync
}

application_running() {
    systemctl is-active --quiet "${SERVICE}" || return 1
    pidof IBHLinkUA >/dev/null 2>&1
}

wait_for_health() {
    local waited=0 stable=0
    while [ "${waited}" -lt "${HEALTH_TIMEOUT}" ]; do
        if application_running; then
            stable=$((stable + 1))
            if [ "${stable}" -ge "${HEALTH_STABLE_SECONDS}" ]; then
                return 0
            fi
        else
            stable=0
        fi
        sleep 1
        waited=$((waited + 1))
    done
    return 1
}

restore_configuration_archive() {
    [ -f "${CONFIG_BACKUP}" ] || return 0
    tar -C / -xzf "${CONFIG_BACKUP}"
}

restore_configuration() {
    [ -f "${CONFIG_BACKUP}" ] || return 0
    mkdir -p "${STATE_DIR}/failed-configuration"
    [ ! -d /etc/ibhlinkua ] || cp -a /etc/ibhlinkua "${STATE_DIR}/failed-configuration/" || true
    [ ! -d /etc/wt ] || cp -a /etc/wt "${STATE_DIR}/failed-configuration/" || true
    restore_configuration_archive
}

rollback() {
    local reason="$1"
    local start_mode="${2:-start_and_verify}"
    local old_package

    write_status rolling_back "${reason}; vorherige Version wird wiederhergestellt."
    log "Rollback started: ${reason}"
    old_package="$(cat "${ROLLBACK_DIR}/package.path" 2>/dev/null || true)"
    [ -n "${old_package}" ] && [ -f "${old_package}" ] || {
        write_status rollback_failed "Rollback-Paket fehlt; manueller Eingriff erforderlich."
        log "Rollback package is missing"
        return 1
    }
    (cd "${ROLLBACK_DIR}" && sha256sum -c package.sha256) >>"${LOG_FILE}" 2>&1 || {
        write_status rollback_failed "Rollback-Paket ist beschaedigt; manueller Eingriff erforderlich."
        return 1
    }

    systemctl stop "${SERVICE}" >>"${LOG_FILE}" 2>&1 || true
    DEBIAN_FRONTEND=noninteractive dpkg --configure -a >>"${LOG_FILE}" 2>&1 || true
    DEBIAN_FRONTEND=noninteractive IBHLINKUA_UPDATE_TRANSACTION=1 \
        apt-get -o Dpkg::Options::=--force-confold install -y --allow-downgrades "${old_package}" >>"${LOG_FILE}" 2>&1 || {
        write_status rollback_failed "Die vorherige Programmversion konnte nicht installiert werden."
        return 1
    }
    restore_configuration
    systemctl daemon-reload >>"${LOG_FILE}" 2>&1 || true
    systemctl enable "${SERVICE}" >>"${LOG_FILE}" 2>&1 || true

    if [ "${start_mode}" = defer_to_boot ]; then
        rm -f "${PENDING_PACKAGE}"
        write_status rolled_back "Unterbrochenes Update wurde automatisch zurueckgerollt; die vorherige Version wird jetzt gestartet."
        log "Boot recovery rollback completed successfully; service start is deferred to systemd"
        return 0
    fi

    systemctl restart "${SERVICE}" >>"${LOG_FILE}" 2>&1 || true
    if wait_for_health; then
        rm -f "${PENDING_PACKAGE}"
        write_status rolled_back "Update fehlgeschlagen; die vorherige Version wurde automatisch wiederhergestellt."
        log "Rollback completed successfully"
        return 0
    fi

    write_status rollback_failed "Auch die wiederhergestellte Version bestand den Health-Check nicht."
    log "Rollback version failed its health check"
    return 1
}

run_update() {
    local new_version

    flock -n 9 || { log "Another update operation is active"; exit 1; }
    validate_package "${PENDING_PACKAGE}" || { write_status validation_failed "Das bereitgestellte Update ist nicht mehr gueltig."; exit 1; }
    new_version="$(package_field "${PENDING_PACKAGE}" Version)"
    write_status preparing "Rollback-Paket und Konfiguration werden gesichert."
    backup_installed_version || {
        write_status backup_failed "Rollback-Sicherung konnte nicht erstellt werden; Update wurde nicht installiert."
        exit 1
    }

    write_status installing "Version ${new_version} wird installiert."
    log "Installing version ${new_version}"
    if ! DEBIAN_FRONTEND=noninteractive IBHLINKUA_UPDATE_TRANSACTION=1 \
        apt-get -o Dpkg::Options::=--force-confold install -y "${PENDING_PACKAGE}" >>"${LOG_FILE}" 2>&1; then
        rollback "Paketinstallation fehlgeschlagen" || true
        exit 1
    fi

    if ! restore_configuration_archive >>"${LOG_FILE}" 2>&1; then
        rollback "Gesicherte Konfiguration konnte nach der Installation nicht wiederhergestellt werden" || true
        exit 1
    fi

    systemctl daemon-reload >>"${LOG_FILE}" 2>&1 || true
    systemctl enable "${SERVICE}" >>"${LOG_FILE}" 2>&1 || true
    systemctl restart "${SERVICE}" >>"${LOG_FILE}" 2>&1 || true
    write_status verifying "Neue Version wird durch einen Health-Check geprueft."
    if ! wait_for_health; then
        rollback "Health-Check der neuen Version fehlgeschlagen" || true
        exit 1
    fi

    install -m 0600 "${PENDING_PACKAGE}" "${STATE_DIR}/installed-${new_version}.deb"
    rm -f "${PENDING_PACKAGE}"
    write_status success "Version ${new_version} wurde erfolgreich installiert."
    log "Update to version ${new_version} completed successfully"
}

recover_update() {
    local state
    flock -n 9 || exit 0
    state="$(sed -n 's/^state=//p' "${STATUS_FILE}" 2>/dev/null | head -n1)"
    case "${state}" in
        installing|verifying|rolling_back)
            rollback "Ein unterbrochenes Update wurde beim Systemstart erkannt" defer_to_boot || true
            ;;
    esac
}

cancel_update() {
    flock -n 9 || exit 1
    rm -f "${PENDING_PACKAGE}" "${PENDING_PACKAGE}.new"
    write_status canceled "Das bereitgestellte Update wurde nicht installiert."
    log "Staged update canceled"
}

case "${1:-}" in
    validate)
        [ "$#" -eq 2 ] || exit 2
        stage_package "$2"
        ;;
    run)
        run_update
        ;;
    recover)
        recover_update
        ;;
    cancel)
        cancel_update
        ;;
    status)
        [ -f "${STATUS_FILE}" ] && cat "${STATUS_FILE}"
        ;;
    *)
        echo "usage: $0 {validate PACKAGE|run|recover|cancel|status}" >&2
        exit 2
        ;;
esac
