#!/bin/sh
#
# sysupgrade — DCENTos firmware self-update (A/B slot) — am2-s17plus variant
# D-Central Technologies — DCENTos Hacker Shell
#
# This script OVERRIDES the shared S9 sysupgrade on am2-s17plus builds via
# Buildroot overlay-on-overlay. The shared S9 script stays intact for S9.
#
# NOTE — image↔chassis matching is the operator's responsibility: every
# BM1397/BM1362 am2 Zynq miner (the whole 17/19 series) reports
# /etc/bos_platform = "zynq-bm3-am2", so the BraiinsOS first-flash fallback
# below cannot tell them apart by platform string alone. On a clean DCENT_OS rootfs the
# /etc/dcentos/board_target overlay file ("am2-s17plus") disambiguates; on a
# first-flash from BraiinsOS the shared platform marker is refused by
# default. Controlled lab first-flash can opt in only with
# DCENT_ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM=1 after confirming the
# physical S17+ chassis.
#
# Differences from the S9 (am1-s9) variant:
#   1. Board name is hard-pinned to "am2-s17plus" (prefix = sysupgrade-am2-s17plus/).
#      Wrong prefix = brick (feedback_sysupgrade_board_name.md).
#   2. Expected UBI volume layout on the inactive slot is 23/179/210 LEBs
#      (kernel/rootfs/rootfs_data) — inherited verbatim from the am2-s19j
#      Phase 1 live probe of .139 (same Zynq 7007S NAND geometry). NOT yet
#      confirmed on a live S17+ (none on the fleet).
#      S9 uses 25/166/525 — not compatible on am2.
#   3. Pre-flight refuses to write if inactive slot volumes do NOT match the
#      inherited am2 template — prevents the mismatch brick mode documented
#      in feedback_ubi_inactive_slot_volume_mismatch.md.
#   4. Does NOT invoke `bos firmware upgrade` — BraiinsOS signed manifest
#      (8 usign keys) rejects unsigned tarballs. We bypass by writing
#      the inactive MTD directly and flipping firmware= via fw_setenv
#      (libubootenv, redundant-copy-atomic — see Step 4; NEVER raw mtd4
#      dd/flash_erase/nandwrite, which bricked the am2 control board twice).
#   5. Preserves the S9 dedicated slot conventions: mtd7=firmware1, mtd8=firmware2.
#      Inherited am2 layout — confirmed identical on S19j Pro .139, assumed
#      identical across the BM1397 17 family (same 7007S control board).
#
# Usage: sysupgrade [-f] [-T] [-N] <rootfs.squashfs|dcentos-sysupgrade-am2-s17plus.tar>
#   -f / --force    Skip confirmation
#   --stop-miner   Gracefully stop mining after validation, before the update
#   -T / --test     Verify package only, no NAND writes
#   -N / --dry-run  Run through every check EXCEPT mtd writes + env flip
#                   (reports the inactive UBI layout so operators can sanity-
#                    check Phase 3 readiness).
#

set -e

ROOTFS=""
ROOTFS_ORIGINAL=""
PACKAGE_INPUT_IS_TAR=0
FORCE=0
STOP_MINER=0
TEST_ONLY=0
DRY_RUN=0
PACKAGE_DIR=""
PACKAGE_KERNEL=""
PACKAGE_MANIFEST=""
PACKAGE_SIG=""
PACKAGE_RELEASE_KEY=""
PACKAGE_STATUS=""
ALLOW_LAB_UNSIGNED=${DCENT_ALLOW_UNSIGNED_SYSUPGRADE:-0}
ALLOW_DOWNGRADE=${DCENT_ALLOW_DOWNGRADE:-0}
ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM=${DCENT_ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM:-0}
RELEASE_PUBKEY="/etc/dcentos/release_ed25519.pub"
VERSION_PATH="/etc/dcentos-version"
BOARD_TARGET_PATH="/etc/dcentos/board_target"
BOS_PLATFORM_PATH="/etc/bos_platform"
PROC_CMDLINE_PATH="/proc/cmdline"
WRONG_BOARD_EXIT=78
PERSIST_HELPER="/usr/libexec/dcentos/sysupgrade-persistent-state.sh"
TRANSACTION_LOCK_HELPER="/usr/libexec/dcentos/sysupgrade-transaction-lock.sh"
TRANSACTION_WORKSPACE_HELPER="/usr/libexec/dcentos/sysupgrade-transaction-workspace.sh"
PACKAGE_INPUT_HELPER="/usr/libexec/dcentos/sysupgrade-package-input.sh"
ARCHIVE_ADMISSION_HELPER="/usr/libexec/dcentos/sysupgrade-archive-admission.sh"
MANIFEST_JSON_HELPER="/usr/libexec/dcentos/sysupgrade-manifest-json.py"
UBI_IDENTITY_HELPER="/usr/libexec/dcentos/sysupgrade-ubi-identity.sh"
UBI_NODE_HELPER="/usr/libexec/dcentos/sysupgrade-ubi-node.sh"
ZYNQ_GEOMETRY_HELPER="/usr/libexec/dcentos/sysupgrade-zynq-geometry.sh"
UBOOT_ENV_ADMISSION_HELPER="/usr/libexec/dcentos/sysupgrade-uboot-env-admission.sh"
SESSION_LATCH_HELPER="/usr/libexec/dcentos/dcentrald-session-latch.sh"
AM2_OTA_HELPER="/usr/libexec/dcentos/am2-native-ota.sh"
DEPLOY_LOCK_HELPER="/usr/libexec/dcentos/dcentos-deploy-lock"
DEPLOY_UPGRADE_GUARD="/usr/libexec/dcentos/dcentrald-deploy-upgrade-guard.sh"
FW_ENV_CONFIG="/etc/fw_env.config"
UBOOT_ENV_PROC_MTD="/proc/mtd"
UBOOT_ENV_SYSFS_MTD_ROOT="/sys/class/mtd"
UBOOT_ENV_MTD4_DEVICE="/dev/mtd4"
PERSIST_SOURCE_ROOT="/data"
PERSIST_MOUNT_ROOT=""
PROC_MOUNTS_PATH="/proc/mounts"
SYSUPGRADE_LOCK_DIR="/run/dcentos-sysupgrade.lock"
SYSUPGRADE_WORKSPACE_ROOT="/tmp"
PROC_ROOT="/proc"
BOOT_ID_PATH="/proc/sys/kernel/random/boot_id"
if [ "${DCENT_SYSUPGRADE_OFFLINE_HARNESS:-0}" = "1" ]; then
    SCRIPT_REALPATH=$(readlink -f "$0" 2>/dev/null || printf '%s\n' "$0")
    if [ "$SCRIPT_REALPATH" = "/usr/sbin/sysupgrade" ]; then
        echo "Error: offline harness overrides are disabled on deployed /usr/sbin/sysupgrade"
        exit 1
    fi
    if [ -z "${DCENT_SYSUPGRADE_OFFLINE_MARKER:-}" ] || [ ! -f "$DCENT_SYSUPGRADE_OFFLINE_MARKER" ]; then
        echo "Error: DCENT_SYSUPGRADE_OFFLINE_HARNESS requires DCENT_SYSUPGRADE_OFFLINE_MARKER"
        exit 1
    fi
    MARKER_VALUE=$(cat "$DCENT_SYSUPGRADE_OFFLINE_MARKER" 2>/dev/null || true)
    if [ "$MARKER_VALUE" != "dcent-sysupgrade-offline-nandsim-harness-v1" ]; then
        echo "Error: invalid DCENT_SYSUPGRADE_OFFLINE_MARKER"
        exit 1
    fi
    RELEASE_PUBKEY="${DCENT_SYSUPGRADE_RELEASE_PUBKEY:-$RELEASE_PUBKEY}"
    VERSION_PATH="${DCENT_SYSUPGRADE_VERSION_PATH:-$VERSION_PATH}"
    BOARD_TARGET_PATH="${DCENT_SYSUPGRADE_BOARD_TARGET_PATH:-$BOARD_TARGET_PATH}"
    BOS_PLATFORM_PATH="${DCENT_SYSUPGRADE_BOS_PLATFORM_PATH:-$BOS_PLATFORM_PATH}"
    PROC_CMDLINE_PATH="${DCENT_SYSUPGRADE_PROC_CMDLINE_PATH:-$PROC_CMDLINE_PATH}"
    PERSIST_HELPER="${DCENT_SYSUPGRADE_PERSIST_HELPER:-$PERSIST_HELPER}"
    TRANSACTION_LOCK_HELPER="${DCENT_SYSUPGRADE_TRANSACTION_LOCK_HELPER:-$TRANSACTION_LOCK_HELPER}"
    TRANSACTION_WORKSPACE_HELPER="${DCENT_SYSUPGRADE_TRANSACTION_WORKSPACE_HELPER:-$TRANSACTION_WORKSPACE_HELPER}"
    PACKAGE_INPUT_HELPER="${DCENT_SYSUPGRADE_PACKAGE_INPUT_HELPER:-$PACKAGE_INPUT_HELPER}"
    ARCHIVE_ADMISSION_HELPER="${DCENT_SYSUPGRADE_ARCHIVE_ADMISSION_HELPER:-$ARCHIVE_ADMISSION_HELPER}"
    MANIFEST_JSON_HELPER="${DCENT_SYSUPGRADE_MANIFEST_JSON_HELPER:-$MANIFEST_JSON_HELPER}"
    UBI_IDENTITY_HELPER="${DCENT_SYSUPGRADE_UBI_IDENTITY_HELPER:-$UBI_IDENTITY_HELPER}"
    UBI_NODE_HELPER="${DCENT_SYSUPGRADE_UBI_NODE_HELPER:-$UBI_NODE_HELPER}"
    ZYNQ_GEOMETRY_HELPER="${DCENT_SYSUPGRADE_ZYNQ_GEOMETRY_HELPER:-$ZYNQ_GEOMETRY_HELPER}"
    UBOOT_ENV_ADMISSION_HELPER="${DCENT_SYSUPGRADE_UBOOT_ENV_ADMISSION_HELPER:-$UBOOT_ENV_ADMISSION_HELPER}"
    SESSION_LATCH_HELPER="${DCENT_SYSUPGRADE_SESSION_LATCH_HELPER:-$SESSION_LATCH_HELPER}"
    AM2_OTA_HELPER="${DCENT_SYSUPGRADE_AM2_OTA_HELPER:-$AM2_OTA_HELPER}"
    FW_ENV_CONFIG="${DCENT_SYSUPGRADE_FW_ENV_CONFIG:-$FW_ENV_CONFIG}"
    UBOOT_ENV_PROC_MTD="${DCENT_SYSUPGRADE_UBOOT_ENV_PROC_MTD:-$UBOOT_ENV_PROC_MTD}"
    UBOOT_ENV_SYSFS_MTD_ROOT="${DCENT_SYSUPGRADE_UBOOT_ENV_SYSFS_MTD_ROOT:-$UBOOT_ENV_SYSFS_MTD_ROOT}"
    UBOOT_ENV_MTD4_DEVICE="${DCENT_SYSUPGRADE_UBOOT_ENV_MTD4_DEVICE:-$UBOOT_ENV_MTD4_DEVICE}"
    PERSIST_SOURCE_ROOT="${DCENT_SYSUPGRADE_PERSIST_SOURCE_ROOT:-$PERSIST_SOURCE_ROOT}"
    PROC_MOUNTS_PATH="${DCENT_SYSUPGRADE_PROC_MOUNTS_PATH:-$PROC_MOUNTS_PATH}"
    SYSUPGRADE_LOCK_DIR="${DCENT_SYSUPGRADE_LOCK_DIR:-$SYSUPGRADE_LOCK_DIR}"
    PROC_ROOT="${DCENT_SYSUPGRADE_PROC_ROOT:-$PROC_ROOT}"
    BOOT_ID_PATH="${DCENT_SYSUPGRADE_BOOT_ID_PATH:-$BOOT_ID_PATH}"
fi

if [ "${DCENT_SYSUPGRADE_OFFLINE_HARNESS:-0}" != "1" ]; then
    if [ "${DCENT_SYSUPGRADE_DEPLOY_LOCK_HELD:-0}" != "1" ]; then
        [ -x "$DEPLOY_LOCK_HELPER" ] || {
            echo "Error: deploy exclusion helper is missing: $DEPLOY_LOCK_HELPER"
            exit 1
        }
        exec "$DEPLOY_LOCK_HELPER" -- env DCENT_SYSUPGRADE_DEPLOY_LOCK_HELD=1 "$0" "$@"
    fi
    [ "$(readlink "/proc/$PPID/exe" 2>/dev/null)" = "$DEPLOY_LOCK_HELPER" ] || {
        echo "Error: sysupgrade deploy-lock provenance is invalid"
        exit 1
    }
fi

if [ ! -r "$PERSIST_HELPER" ]; then
    echo "Error: persistent-state helper is missing or unreadable: $PERSIST_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-persistent-state.sh
. "$PERSIST_HELPER"
if ! command -v dcent_persist_preflight >/dev/null 2>&1 ||
   ! command -v dcent_persist_stage >/dev/null 2>&1 ||
   ! command -v dcent_persist_verify >/dev/null 2>&1; then
    echo "Error: persistent-state helper did not provide its required API"
    exit 1
fi
if [ ! -r "$TRANSACTION_LOCK_HELPER" ]; then
    echo "Error: sysupgrade transaction-lock helper is missing or unreadable: $TRANSACTION_LOCK_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-transaction-lock.sh
. "$TRANSACTION_LOCK_HELPER"
if ! command -v dcent_sysupgrade_lock_acquire >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_lock_arm_env_commit >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_lock_abort_env_commit >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_lock_require_cleanup >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_lock_preserve >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_lock_release >/dev/null 2>&1; then
    echo "Error: sysupgrade transaction-lock helper did not provide its required API"
    exit 1
fi
if [ ! -r "$TRANSACTION_WORKSPACE_HELPER" ]; then
    echo "Error: sysupgrade transaction-workspace helper is missing or unreadable: $TRANSACTION_WORKSPACE_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-transaction-workspace.sh
. "$TRANSACTION_WORKSPACE_HELPER"
if ! command -v dcent_sysupgrade_workspace_create >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_workspace_path >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_workspace_require_absent >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_workspace_cleanup >/dev/null 2>&1; then
    echo "Error: sysupgrade transaction-workspace helper did not provide its required API"
    exit 1
fi
if [ ! -r "$PACKAGE_INPUT_HELPER" ]; then
    echo "Error: sysupgrade package-input helper is missing or unreadable: $PACKAGE_INPUT_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-package-input.sh
. "$PACKAGE_INPUT_HELPER"
if ! command -v dcent_sysupgrade_input_open >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_input_verify_unchanged >/dev/null 2>&1 ||
   ! command -v dcent_sysupgrade_input_close >/dev/null 2>&1; then
    echo "Error: sysupgrade package-input helper did not provide its required API"
    exit 1
fi
if [ ! -r "$ARCHIVE_ADMISSION_HELPER" ]; then
    echo "Error: sysupgrade archive-admission helper is missing or unreadable: $ARCHIVE_ADMISSION_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-archive-admission.sh
. "$ARCHIVE_ADMISSION_HELPER"
if ! command -v dcent_sysupgrade_archive_admit >/dev/null 2>&1; then
    echo "Error: sysupgrade archive-admission helper did not provide its required API"
    exit 1
fi
if [ ! -r "$MANIFEST_JSON_HELPER" ] || ! command -v python3 >/dev/null 2>&1; then
    echo "Error: semantic manifest admission requires python3 and $MANIFEST_JSON_HELPER"
    exit 1
fi
if [ ! -r "$UBI_IDENTITY_HELPER" ]; then
    echo "Error: UBI identity helper is missing or unreadable: $UBI_IDENTITY_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-ubi-identity.sh
. "$UBI_IDENTITY_HELPER"
if ! command -v dcent_ubi_attachment_require_absent >/dev/null 2>&1 ||
   ! command -v dcent_ubi_identity_admit >/dev/null 2>&1; then
    echo "Error: UBI identity helper did not provide its required API"
    exit 1
fi
if [ ! -r "$UBI_NODE_HELPER" ]; then
    echo "Error: UBI node helper is missing or unreadable: $UBI_NODE_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-ubi-node.sh
. "$UBI_NODE_HELPER"
if ! command -v dcent_ubi_node_admit >/dev/null 2>&1; then
    echo "Error: UBI node helper did not provide its required API"
    exit 1
fi

dcent_ubi_attach_mtd() {
    dcent_ubi_node_admit /sys/class/misc/ubi_ctrl/dev /dev ubi_ctrl || return 1
    ubiattach -m "$1" -d "$2" -O 2048 --max-beb-per1024=20
}

dcent_ubi_detach_device() {
    dcent_ubi_node_admit /sys/class/misc/ubi_ctrl/dev /dev ubi_ctrl || return 1
    ubidetach -d "$1"
}

dcent_ubi_device_admit() {
    dcent_ubi_node_admit "/sys/class/ubi/ubi$1/dev" /dev "ubi$1"
}

dcent_ubi_volume_admit() {
    dcent_ubi_node_admit "/sys/class/ubi/ubi${1}_${2}/dev" /dev "ubi${1}_${2}"
}

dcent_ubi_semantic_identity_admit() {
    dcent_ubi_identity_admit /sys/class/ubi "$1" "$2" 3 \
        dynamic dynamic dynamic
}

dcent_ubi_make_volume() {
    _dcent_ubi_make_device=$1
    shift
    dcent_ubi_node_admit "/sys/class/ubi/ubi$_dcent_ubi_make_device/dev" \
        /dev "ubi$_dcent_ubi_make_device" || return 1
    ubimkvol "/dev/ubi$_dcent_ubi_make_device" "$@"
}

dcent_ubi_update_volume() {
    _dcent_ubi_update_device=$1
    _dcent_ubi_update_volume=$2
    _dcent_ubi_update_source=$3
    dcent_ubi_semantic_identity_admit "$_dcent_ubi_update_device" \
        "$INACTIVE_MTD" || return 1
    dcent_ubi_volume_admit "$_dcent_ubi_update_device" \
        "$_dcent_ubi_update_volume" || return 1
    ubiupdatevol \
        "/dev/ubi${_dcent_ubi_update_device}_${_dcent_ubi_update_volume}" \
        "$_dcent_ubi_update_source"
}
if [ ! -r "$UBOOT_ENV_ADMISSION_HELPER" ]; then
    echo "Error: U-Boot environment admission helper is missing or unreadable: $UBOOT_ENV_ADMISSION_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-uboot-env-admission.sh
. "$UBOOT_ENV_ADMISSION_HELPER"
if ! command -v dcent_zynq_uboot_env_admit >/dev/null 2>&1; then
    echo "Error: U-Boot environment admission helper did not provide its required API"
    exit 1
fi
if [ ! -r "$SESSION_LATCH_HELPER" ]; then
    echo "Error: hardware-session latch helper is missing or unreadable: $SESSION_LATCH_HELPER"
    exit 1
fi

# --- am2-s17plus expected UBI volume layout ---
# Volumes: 0=kernel, 1=rootfs, 2=rootfs_data
# NOTE: the LEB template below is inherited verbatim from the am2-s19j
# probe of .139 (same Zynq 7007S NAND geometry). No live 17-series unit on
# the fleet, so this is UNCONFIRMED on real S17 hardware — the pre-flight
# brick gate still refuses any inactive slot that doesn't match it.
EXPECTED_BOARD="am2-s17plus"
EXPECTED_KERNEL_LEBS=23
EXPECTED_ROOTFS_LEBS=179
EXPECTED_ROOTFS_DATA_LEBS=210
# Volumes smaller than the template are refused because a stock-sized payload
# may not fit. Larger rootfs volumes remain subject to the total-layout and
# per-payload capacity checks below. The runtime kernel layout accepts ±4 LEBs,
# but package compatibility remains the exact 23-LEB stock window.
# rootfs_data is operator-managed; a mismatch warns but does not refuse.
KERNEL_LEB_TOLERANCE=4

cleanup_package() {
    cleanup_status=$?
    trap - EXIT HUP INT TERM
    if ! dcent_sysupgrade_input_close; then
        echo "Error: sysupgrade package descriptor could not be closed safely" >&2
        [ "$cleanup_status" -ne 0 ] || cleanup_status=1
    fi
    if ! dcent_sysupgrade_workspace_cleanup "$PROC_MOUNTS_PATH"; then
        echo "Error: sysupgrade workspace cleanup is ambiguous; preserving workspace and transaction lock" >&2
        case "${DCENT_SYSUPGRADE_LOCK_PHASE:-}" in
            active)
                dcent_sysupgrade_lock_require_cleanup || \
                    echo "Error: could not publish cleanup-required transaction state" >&2
                ;;
        esac
        [ "$cleanup_status" -ne 0 ] || cleanup_status=1
    elif ! dcent_sysupgrade_lock_release; then
        echo "Error: sysupgrade transaction lock could not be released safely" >&2
        [ "$cleanup_status" -ne 0 ] || cleanup_status=1
    fi
    exit "$cleanup_status"
}

verify_sysupgrade_input_unchanged() {
    [ "${DCENT_SYSUPGRADE_INPUT_OPEN:-0}" = 1 ] || return 0
    if ! dcent_sysupgrade_input_verify_unchanged; then
        echo "Error: sysupgrade input changed during its active read window: $ROOTFS_ORIGINAL"
        return 1
    fi
}

verify_and_close_sysupgrade_input() {
    verify_sysupgrade_input_unchanged || return 1
    [ "${DCENT_SYSUPGRADE_INPUT_OPEN:-0}" = 1 ] || return 0
    dcent_sysupgrade_input_close
}

manifest_field() {
    # Extract a dotted JSON field. Prefer jsonfilter (present on BraiinsOS +
    # OpenWrt) over python3 (present on DCENT_OS rootfs). Needed because
    # first-flash from BraiinsOS doesn't have python3 available.
    if command -v jsonfilter >/dev/null 2>&1; then
        jsonfilter -i "$1" -e "@.$2" 2>/dev/null
    elif command -v python3 >/dev/null 2>&1; then
        python3 - "$1" "$2" <<'PY'
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as fh:
    data = json.load(fh)
node = data
for key in sys.argv[2].split('.'):
    node = node[key]
if isinstance(node, bool):
    print('true' if node else 'false')
else:
    print(node)
PY
    else
        echo "Error: manifest_field needs jsonfilter or python3" >&2
        return 1
    fi
}

manifest_string_field() {
    file=$1
    key=$2
    [ "$(manifest_key_count "$file" "$key")" = "1" ] || return 1
    sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$file" | head -n 1
}

manifest_key_count() {
    file=$1
    key=$2
    awk -v needle="\"$key\"" '
        {
            line = $0
            while ((position = index(line, needle)) > 0) {
                count++
                line = substr(line, position + length(needle))
            }
        }
        END { print count + 0 }
    ' "$file"
}

manifest_boolean_field() {
    file=$1
    key=$2
    [ "$(manifest_key_count "$file" "$key")" = "1" ] || return 1
    sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*\(true\|false\)[[:space:]]*[,}].*/\1/p' "$file" | head -n 1
}

manifest_integer_field() {
    file=$1
    key=$2
    [ "$(manifest_key_count "$file" "$key")" = "1" ] || return 1
    sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\)[[:space:]]*[,}].*/\1/p' "$file" | head -n 1
}

is_release_status() {
    case "${1:-release}" in
        release|production|stable) return 0 ;;
        *) return 1 ;;
    esac
}

is_truthy() {
    case "${1:-}" in
        1|true|TRUE|yes|YES|y|Y) return 0 ;;
        *) return 1 ;;
    esac
}

if is_truthy "$ALLOW_LAB_UNSIGNED"; then
    ALLOW_LAB_UNSIGNED=1
else
    ALLOW_LAB_UNSIGNED=0
fi
if is_truthy "$ALLOW_DOWNGRADE"; then
    ALLOW_DOWNGRADE=1
else
    ALLOW_DOWNGRADE=0
fi
if is_truthy "$ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM"; then
    ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM=1
else
    ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM=0
fi

read_current_version() {
    python3 "$MANIFEST_JSON_HELPER" read-version-file "$VERSION_PATH"
}

compare_versions() {
    python3 "$MANIFEST_JSON_HELPER" compare-version "$1" "$2"
}

allow_downgrade_override() {
    [ "$ALLOW_DOWNGRADE" = "1" ] && ! is_release_status "$PACKAGE_STATUS"
}

enforce_sysupgrade_version_floor() {
    candidate_version=$(manifest_string_field "$PACKAGE_MANIFEST" version 2>/dev/null || true)
    if [ -z "$candidate_version" ]; then
        echo "Error: Refusing sysupgrade: package MANIFEST.json has no single non-empty string version; rollback floor cannot be evaluated."
        return 1
    fi
    candidate_version_trimmed=$(printf '%s' "$candidate_version" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    if [ "$candidate_version" != "$candidate_version_trimmed" ]; then
        echo "Error: Refusing sysupgrade: package version must not contain surrounding whitespace."
        return 1
    fi

    current_version=$(read_current_version || true)
    if [ -z "$current_version" ]; then
        echo "Error: Refusing sysupgrade: current $VERSION_PATH is missing or empty; rollback floor cannot be evaluated."
        return 1
    fi

    if ! version_cmp=$(compare_versions "$candidate_version" "$current_version"); then
        echo "Error: Refusing sysupgrade: unparseable firmware version (candidate=$candidate_version current=$current_version)."
        return 1
    fi

    case "$version_cmp" in
        -1)
            if allow_downgrade_override; then
                echo "  WARNING: allowing non-release downgrade $current_version -> $candidate_version because DCENT_ALLOW_DOWNGRADE=1"
                return 0
            fi
            echo "Error: Downgrade refused: package version $candidate_version is older than the running firmware version $current_version. DCENT_OS denies firmware downgrades by default."
            return 1
            ;;
        0)
            echo "  OK: package version $candidate_version matches running firmware version $current_version"
            ;;
        1)
            echo "  OK: package version $candidate_version is newer than running firmware version $current_version"
            ;;
        *)
            echo "Error: Refusing sysupgrade: unparseable firmware version (candidate=$candidate_version current=$current_version)."
            return 1
            ;;
    esac
}

verify_sha256() {
    actual=$(sha256sum "$1" | awk '{print $1}')
    [ "$actual" = "$2" ]
}

manifest_payload_block() {
    _payload_kind=$1
    awk -v kind="$_payload_kind" '
        BEGIN {
            RS = "}"
            declaration = "\"" kind "\"[[:space:]]*:[[:space:]]*\\{"
        }
        $0 ~ declaration && index($0, "\"path\"") {
            count++
            block = $0 "}"
        }
        END {
            if (count == 1) print block
            exit count == 1 ? 0 : 1
        }
    ' "$PACKAGE_MANIFEST"
}

manifest_payload_block_key_count() {
    _payload_block=$1
    _payload_field=$2
    printf '%s\n' "$_payload_block" | awk -v needle="\"$_payload_field\"" '
        {
            line = $0
            while ((position = index(line, needle)) > 0) {
                count++
                line = substr(line, position + length(needle))
            }
        }
        END { print count + 0 }
    '
}

manifest_payload_string_from_block() {
    _payload_block=$1
    _payload_field=$2
    printf '%s\n' "$_payload_block" \
        | sed -n 's/.*"'"$_payload_field"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
        | head -n 1
}

manifest_payload_integer_from_block() {
    _payload_block=$1
    _payload_field=$2
    printf '%s\n' "$_payload_block" \
        | sed -n 's/.*"'"$_payload_field"'"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\)[[:space:]]*[,}].*/\1/p' \
        | head -n 1
}

validate_extracted_package_leaves() {
    _nested_entry=$(find "$PACKAGE_SUBDIR" -mindepth 2 -print 2>/dev/null | sed -n '1p')
    if [ -n "$_nested_entry" ]; then
        echo "Error: Extracted package contains nested payload entry '$_nested_entry'"
        return 1
    fi

    _unknown_entry=$(find "$PACKAGE_SUBDIR" -mindepth 1 -maxdepth 1 -print 2>/dev/null | awk -F/ '
        {
            leaf = $NF
            if (leaf != "kernel" && leaf != "root" && leaf != "METADATA" &&
                leaf != "SHA256SUMS" && leaf != "MANIFEST.json" &&
                leaf != "MANIFEST.sig" && leaf != "release_ed25519.pub" &&
                leaf != "fpga_bitstream.bit") {
                print
                exit
            }
        }
    ')
    if [ -n "$_unknown_entry" ]; then
        echo "Error: Extracted package contains unknown payload leaf '$_unknown_entry'"
        return 1
    fi

    for _payload_leaf in kernel root METADATA SHA256SUMS MANIFEST.json MANIFEST.sig release_ed25519.pub fpga_bitstream.bit; do
        _payload_file="$PACKAGE_SUBDIR/$_payload_leaf"
        if [ -e "$_payload_file" ] || [ -L "$_payload_file" ]; then
            if [ ! -f "$_payload_file" ] || [ -L "$_payload_file" ]; then
                echo "Error: Extracted package leaf '$_payload_leaf' must be a direct regular file"
                return 1
            fi
        fi
    done
    return 0
}

validate_manifest_payload_binding() {
    _payload_kind=$1
    _payload_leaf=$2
    _payload_file=$3
    _payload_block=$(manifest_payload_block "$_payload_kind") || {
        echo "Error: Package manifest must contain one structured '$_payload_kind' payload"
        return 1
    }
    for _payload_field in path size sha256; do
        [ "$(manifest_payload_block_key_count "$_payload_block" "$_payload_field")" = "1" ] || {
            echo "Error: Package '$_payload_kind' payload must contain exactly one '$_payload_field' field"
            return 1
        }
    done

    _payload_path=$(manifest_payload_string_from_block "$_payload_block" path)
    _payload_expected_path="$PACKAGE_SUBDIR_NAME/$_payload_leaf"
    [ "$_payload_path" = "$_payload_expected_path" ] || {
        echo "Error: Package '$_payload_kind' payload path must be exactly '$_payload_expected_path'"
        return 1
    }

    _payload_size=$(manifest_payload_integer_from_block "$_payload_block" size)
    case "$_payload_size" in
        ''|*[!0-9]*|0)
            echo "Error: Package '$_payload_kind' payload size must be a positive JSON integer"
            return 1
            ;;
    esac
    if [ ! -f "$_payload_file" ] || [ -L "$_payload_file" ]; then
        echo "Error: Package '$_payload_kind' payload must resolve to a direct regular file"
        return 1
    fi
    _payload_actual_size=$(wc -c < "$_payload_file" | tr -d '[:space:]')
    [ "$_payload_actual_size" = "$_payload_size" ] || {
        echo "Error: Package '$_payload_kind' payload size does not match signed manifest"
        return 1
    }

    _payload_sha=$(manifest_payload_string_from_block "$_payload_block" sha256)
    _payload_sha_length=$(printf '%s' "$_payload_sha" | wc -c | tr -d '[:space:]')
    if [ "$_payload_sha_length" != "64" ]; then
        echo "Error: Package '$_payload_kind' payload sha256 must be exactly 64 lowercase hexadecimal characters"
        return 1
    fi
    case "$_payload_sha" in
        *[!0123456789abcdef]*)
            echo "Error: Package '$_payload_kind' payload sha256 must be exactly 64 lowercase hexadecimal characters"
            return 1
            ;;
    esac
    if ! verify_sha256 "$_payload_file" "$_payload_sha"; then
        echo "Error: Package '$_payload_kind' payload bytes do not match signed sha256"
        return 1
    fi
    return 0
}

validate_sysupgrade_tar_members() {
    dcent_sysupgrade_archive_admit \
        "$1" "$EXPECTED_BOARD" \
        "${DCENT_SYSUPGRADE_WORKSPACE:-${TMPDIR:-/tmp}}"
}

if [ ! -r "$ZYNQ_GEOMETRY_HELPER" ]; then
    echo "Error: canonical Zynq geometry helper is missing or unreadable: $ZYNQ_GEOMETRY_HELPER"
    exit 1
fi
# shellcheck source=/usr/libexec/dcentos/sysupgrade-zynq-geometry.sh
. "$ZYNQ_GEOMETRY_HELPER"
if ! command -v dcent_zynq_geometry_require_payload_fit >/dev/null 2>&1 ||
   ! command -v dcent_zynq_geometry_tar_preextract_ceiling >/dev/null 2>&1; then
    echo "Error: canonical Zynq geometry helper did not provide its required API"
    exit 1
fi
SYSUPGRADE_TAR_SLACK_BYTES=$ZYNQ_SYSUPGRADE_TAR_SLACK_BYTES

sysupgrade_tar_preextract_ceiling() {
    dcent_zynq_geometry_tar_preextract_ceiling "$EXPECTED_BOARD"
}

validate_sysupgrade_tar_preextract() {
    local tarball=$1
    local package_size ceiling tmp_avail_kb tmp_avail_bytes tmp_required

    package_size=$(wc -c < "$tarball" | tr -d '[:space:]')
    case "$package_size" in ''|*[!0-9]*|0) echo "Error: cannot validate sysupgrade package size (size=$package_size)"; return 1 ;; esac
    ceiling=$(sysupgrade_tar_preextract_ceiling)
    case "$ceiling" in ''|*[!0-9]*|0) echo "Error: cannot validate sysupgrade package ceiling for $EXPECTED_BOARD"; return 1 ;; esac

    if [ "$package_size" -gt "$ceiling" ]; then
        echo "Error: sysupgrade package exceeds pre-extraction ceiling ($package_size bytes > $ceiling bytes)."
        echo "  Refusing before tar extraction."
        return 1
    fi

    tmp_avail_kb=$(df -Pk "$DCENT_SYSUPGRADE_WORKSPACE" 2>/dev/null | awk 'NR==2 {print $4}')
    case "$tmp_avail_kb" in ''|*[!0-9]*|0) echo "Error: cannot validate workspace free space before tar extraction"; return 1 ;; esac
    tmp_avail_bytes=$((tmp_avail_kb * 1024))
    # `df` free already excludes an uploaded tar resident on this same tmpfs.
    # Packages are uncompressed and require their own kernel, so remaining
    # scratch must hold one extraction of package_size plus filesystem slack.
    # The raw-image path admits its optional active-kernel copy separately.
    tmp_required=$((package_size + SYSUPGRADE_TAR_SLACK_BYTES))
    if [ "$tmp_avail_bytes" -lt "$tmp_required" ]; then
        echo "Error: the private workspace has insufficient free space for sysupgrade package extraction ($tmp_avail_bytes bytes available < $tmp_required bytes required)."
        echo "  Refusing before tar extraction."
        return 1
    fi

    echo "  OK: sysupgrade package pre-extract size $package_size <= $ceiling bytes; workspace free $tmp_avail_bytes bytes"
    return 0
}

workspace_require_free_bytes() {
    _scratch_label=$1
    _scratch_required=$2
    _scratch_avail_kb=$(df -Pk "$DCENT_SYSUPGRADE_WORKSPACE" 2>/dev/null | awk 'NR==2 {print $4}')
    case "$_scratch_avail_kb" in
        ''|*[!0-9]*|0) echo "Error: cannot validate workspace free space for $_scratch_label"; return 1 ;;
    esac
    _scratch_avail=$((_scratch_avail_kb * 1024))
    if [ "$_scratch_avail" -lt "$_scratch_required" ]; then
        echo "Error: private workspace has insufficient free space for $_scratch_label ($_scratch_avail bytes available < $_scratch_required bytes required)."
        return 1
    fi
    return 0
}

# Read volume LEB count for a ubi attachment. $1 = ubi device index (0 or 1),
# $2 = volume index. Echoes the reserved_ebs count, or empty on failure.
ubi_vol_lebs() {
    cat "/sys/class/ubi/ubi$1_$2/reserved_ebs" 2>/dev/null
}

ubi_dev_leb_size() {
    cat "/sys/class/ubi/ubi$1/eraseblock_size" 2>/dev/null
}

payload_fits_ubi_volume() {
    _label=$1
    _size=$2
    _lebs=$3
    _leb_size=$4

    case "$_size" in ''|*[!0-9]*|0) echo "Error: cannot validate $_label payload fit (size=$_size)"; return 1 ;; esac
    case "$_lebs" in ''|*[!0-9]*|0) echo "Error: cannot validate $_label payload fit (lebs=$_lebs)"; return 1 ;; esac
    case "$_leb_size" in ''|*[!0-9]*|0) echo "Error: cannot validate $_label payload fit (leb_size=$_leb_size)"; return 1 ;; esac
    _capacity=$((_lebs * _leb_size))
    if [ "$_size" -gt "$_capacity" ]; then
        echo "Error: $_label payload exceeds inactive UBI volume ($_size bytes > $_capacity bytes)."
        echo "  Refusing before ubiupdatevol; wrong payload/window size = brick."
        return 1
    fi
    echo "  OK: $_label payload fits inactive UBI volume ($_size bytes <= $_capacity bytes)"
    return 0
}

# A signal trap runs with the status of the interrupted command; dash and
# BusyBox ash may therefore present status 0 to a shared cleanup handler.
# Translate signals to conventional nonzero statuses first, then let the one
# EXIT trap perform the exact same resource cleanup path.
trap cleanup_package EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
dcent_sysupgrade_lock_acquire "$SYSUPGRADE_LOCK_DIR" "$PROC_ROOT" "$BOOT_ID_PATH" || exit 1
dcent_sysupgrade_workspace_create "$SYSUPGRADE_WORKSPACE_ROOT" || exit 1
PACKAGE_WORK_DIR=$(dcent_sysupgrade_workspace_path package) || exit 1
KERNEL_TEMP=$(dcent_sysupgrade_workspace_path active-kernel.bin) || exit 1
ENV_PRE_TEMP=$(dcent_sysupgrade_workspace_path uboot-env-pre.txt) || exit 1
FW_SETENV_SCRIPT=$(dcent_sysupgrade_workspace_path fw-setenv.env) || exit 1
PERSIST_MOUNT_ROOT=$(dcent_sysupgrade_workspace_path inactive-data) || exit 1

# Parse arguments
for arg in "$@"; do
    case "$arg" in
        -f|--force)    FORCE=1 ;;
        -T|--test)     TEST_ONLY=1 ;;
        -N|--dry-run)  DRY_RUN=1 ;;
        --stop-miner) STOP_MINER=1 ;;
        -*) echo "Unknown option: $arg"; exit 1 ;;
        *) ROOTFS="$arg" ;;
    esac
done
ROOTFS_ORIGINAL=$ROOTFS

# --- Board pin check ---
# We only run on am2-s17plus hardware. Refuse otherwise (prevents an am2 tarball
# from being fed into a mis-overlaid rootfs).
#
# On a clean DCENT_OS rootfs /etc/dcentos/board_target is present (from the
# overlay, "am2-s17plus"). On BraiinsOS (first-flash scenario) it's absent —
# probing /etc/bos_platform can only see "zynq-bm3-am2". Every BM1397 S17+
# and S19j Pro report that token, so the generic marker is refused by default.
# Controlled lab first-flash may opt in with
# DCENT_ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM=1 after chassis confirmation.
# Test-only/dry-run also fail closed on board mismatches: a green pre-flight on
# the wrong board is misleading and can train unsafe operator behavior.
DETECTED_BOARD=""
BOARD_CHECK_SOURCE=""
if [ -f "$BOARD_TARGET_PATH" ]; then
    DETECTED_BOARD=$(cat "$BOARD_TARGET_PATH" 2>/dev/null || echo unknown)
    BOARD_CHECK_SOURCE="$BOARD_TARGET_PATH"
elif [ -f "$BOS_PLATFORM_PATH" ]; then
    BOS_PLATFORM=$(cat "$BOS_PLATFORM_PATH" 2>/dev/null || echo unknown)
    BOARD_CHECK_SOURCE="$BOS_PLATFORM_PATH=$BOS_PLATFORM"
    case "$BOS_PLATFORM" in
        # zynq-bm3-am2 is reported by the whole 17/19-series am2 family.
        # Clean DCENT_OS uses board_target, which is exact. First-flash from
        # BraiinsOS needs an explicit lab override after chassis confirmation.
        zynq-bm3-am2)
            if [ "$ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM" = "1" ]; then
                echo "  WARNING: accepting ambiguous zynq-bm3-am2 for am2-s17plus because DCENT_ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM=1 is set"
                echo "  Lab override only: confirm the physical chassis is S17+ before any NAND write."
                DETECTED_BOARD="am2-s17plus"
            else
                echo "Error: zynq-bm3-am2 is an ambiguous AM2 platform marker for this am2-s17plus sysupgrade."
                echo "  It can describe any am2 Zynq hardware; /etc/dcentos/board_target=am2-s17plus is required for normal updates."
                echo "  For controlled lab first-flash only, set DCENT_ALLOW_AM2_S17PLUS_AMBIGUOUS_BOS_PLATFORM=1 after confirming the physical S17+ chassis."
                echo "  Refusing before any NAND write. Wrong AM2 image = brick."
                exit 1
            fi
            ;;
        *)            DETECTED_BOARD="bosplatform-${BOS_PLATFORM}" ;;
    esac
fi
if [ "$DETECTED_BOARD" != "$EXPECTED_BOARD" ]; then
    echo "Error: am2 sysupgrade running on wrong board (detected='$DETECTED_BOARD' via $BOARD_CHECK_SOURCE, expected='$EXPECTED_BOARD')"
    echo "  Refusing before any NAND write. Wrong board = brick."
    echo "  Test/dry-run refuses wrong boards with exit $WRONG_BOARD_EXIT so pre-flight cannot report a false green."
    echo "  This is the am2-s17plus variant of sysupgrade. S9 units should use the shared overlay."
    exit "$WRONG_BOARD_EXIT"
fi

# --- Detect current firmware slot ---
CMDLINE=$(cat "$PROC_CMDLINE_PATH")
CURRENT_MTD=$(echo "$CMDLINE" | grep -o 'ubi\.mtd=[0-9]*' | cut -d= -f2)

if [ -z "$CURRENT_MTD" ]; then
    echo "Error: Cannot detect current MTD from /proc/cmdline"
    echo "cmdline: $CMDLINE"
    exit 1
fi

# Same mapping as S9 — Phase 1 confirmed mtd7/mtd8 layout matches on am2.
if [ "$CURRENT_MTD" = "8" ]; then
    INACTIVE_MTD=7
    CURRENT_FW=2
    INACTIVE_FW=1
elif [ "$CURRENT_MTD" = "7" ]; then
    INACTIVE_MTD=8
    CURRENT_FW=1
    INACTIVE_FW=2
else
    echo "Error: Unexpected MTD partition $CURRENT_MTD (expected 7 or 8)"
    exit 1
fi
# --- Validation ---
if [ -z "$ROOTFS" ]; then
    MY_IP=$(ip addr show eth0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)
    echo "Usage: sysupgrade [-f] [-T|--test] [-N|--dry-run] <rootfs.squashfs|dcentos-sysupgrade.tar>"
    echo ""
    echo "Options:"
    echo "  -f, --force    Skip confirmation prompt"
    echo "  -T, --test     Verify package/image only, do not write NAND"
    echo "  --stop-miner   Gracefully stop the managed miner before updating"
    echo "  -N, --dry-run  Run every check (including UBI attach) but skip NAND writes"
    echo ""
    echo "Board:   $EXPECTED_BOARD (S17+ Zynq, BM1397)"
    echo "Current: firmware=$CURRENT_FW (mtd$CURRENT_MTD)"
    echo "Target:  firmware=$INACTIVE_FW (mtd$INACTIVE_MTD)"
    echo ""
    echo "Expected inactive UBI layout (LEBs):"
    echo "  kernel=$EXPECTED_KERNEL_LEBS  rootfs=$EXPECTED_ROOTFS_LEBS  rootfs_data=$EXPECTED_ROOTFS_DATA_LEBS"
    exit 1
fi

case "$ROOTFS_ORIGINAL" in
    /*) ;;
    *)
        echo "Error: sysupgrade input must be an absolute path (for example /tmp/dcentos-sysupgrade.tar)."
        echo "  Relative paths are refused so package identity cannot depend on a mutable working directory."
        exit 1
        ;;
esac
if [ ! -f "$ROOTFS_ORIGINAL" ]; then
    echo "Error: $ROOTFS_ORIGINAL not found"
    exit 1
fi
if ! dcent_sysupgrade_input_open "$ROOTFS_ORIGINAL"; then
    echo "Error: sysupgrade input admission failed for $ROOTFS_ORIGINAL"
    exit 1
fi
ROOTFS=$DCENT_SYSUPGRADE_INPUT_FD_PATH
# Retain the measured package identity before descriptor close clears its state.
AM2_OTA_PACKAGE_SHA256=$DCENT_SYSUPGRADE_INPUT_SHA256

# --- Tar package handling ---
if tar tf "$ROOTFS" >/dev/null 2>&1; then
    PACKAGE_INPUT_IS_TAR=1
    validate_sysupgrade_tar_preextract "$ROOTFS" || exit 1
    validate_sysupgrade_tar_members "$ROOTFS" || exit 1
    echo "Detected sysupgrade package tar — extracting and verifying manifest..."
    PACKAGE_DIR=$PACKAGE_WORK_DIR
    dcent_sysupgrade_workspace_require_absent "$PACKAGE_DIR" || exit 1
    mkdir -m 700 "$PACKAGE_DIR" || {
        echo "Error: Failed to create private package extraction directory"
        exit 1
    }
    if ! tar xf "$ROOTFS" -C "$PACKAGE_DIR"; then
        echo "Error: Failed to extract sysupgrade tar package"
        exit 1
    fi

    # --- Tarball prefix check (CRITICAL — wrong prefix = brick) ---
    # Enumerate top-level directories. Must be exactly one, and must match
    # sysupgrade-$EXPECTED_BOARD/.
    TOP_LEVEL=$(find "$PACKAGE_DIR" -mindepth 1 -maxdepth 1 -type d | sed "s|$PACKAGE_DIR/||")
    TL_COUNT=$(echo "$TOP_LEVEL" | grep -c . || true)
    if [ "$TL_COUNT" != "1" ]; then
        echo "Error: Package must contain exactly one top-level directory (found $TL_COUNT)"
        echo "  Entries: $TOP_LEVEL"
        exit 1
    fi
    case "$TOP_LEVEL" in
        sysupgrade-$EXPECTED_BOARD) ;;
        sysupgrade-am1-s9)
            echo "Error: Package prefix '$TOP_LEVEL' targets S9 (am1-s9) hardware."
            echo "  This is an $EXPECTED_BOARD unit — refusing to flash. Wrong prefix = brick."
            exit 1
            ;;
        sysupgrade-*)
            echo "Error: Package prefix '$TOP_LEVEL' does not match expected '$EXPECTED_BOARD'."
            echo "  Refusing to flash — wrong prefix = brick."
            exit 1
            ;;
        *)
            echo "Error: Package top-level '$TOP_LEVEL' is not a sysupgrade payload."
            exit 1
            ;;
    esac
    PACKAGE_SUBDIR="$PACKAGE_DIR/$TOP_LEVEL"

    PACKAGE_MANIFEST="$PACKAGE_SUBDIR/MANIFEST.json"
    PACKAGE_SIG="$PACKAGE_SUBDIR/MANIFEST.sig"
    PACKAGE_RELEASE_KEY="$PACKAGE_SUBDIR/release_ed25519.pub"
    PACKAGE_KERNEL="$PACKAGE_SUBDIR/kernel"
    ROOTFS="$PACKAGE_SUBDIR/root"
    PACKAGE_SUBDIR_NAME=${PACKAGE_SUBDIR##*/}
    validate_extracted_package_leaves || exit 1

    if [ ! -f "$PACKAGE_MANIFEST" ]; then
        echo "Error: Package is missing MANIFEST.json"
        exit 1
    fi
    if ! python3 "$MANIFEST_JSON_HELPER" validate "$PACKAGE_MANIFEST"; then
        echo "Error: Package manifest failed semantic/canonical JSON admission"
        exit 1
    fi
    [ "$(manifest_key_count "$PACKAGE_MANIFEST" manifest_profile)" = "1" ] || {
        echo "Error: Package manifest must contain exactly one 'manifest_profile' authority field"
        exit 1
    }
    PACKAGE_MANIFEST_PROFILE=$(manifest_string_field "$PACKAGE_MANIFEST" manifest_profile 2>/dev/null || true)
    PACKAGE_STATUS_COUNT=$(manifest_key_count "$PACKAGE_MANIFEST" status)
    [ "$PACKAGE_STATUS_COUNT" = "1" ] || {
        echo "Error: Package manifest must contain exactly one 'status' authority field"
        exit 1
    }
    PACKAGE_STATUS=$(manifest_string_field "$PACKAGE_MANIFEST" status 2>/dev/null || true)
    [ -n "$PACKAGE_STATUS" ] || {
        echo "Error: Package manifest status must be a non-empty string"
        exit 1
    }
    PACKAGE_STATUS_TRIMMED=$(printf '%s' "$PACKAGE_STATUS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    [ "$PACKAGE_STATUS" = "$PACKAGE_STATUS_TRIMMED" ] || {
        echo "Error: Package manifest status must not contain surrounding whitespace"
        exit 1
    }
    if [ ! -f "$PACKAGE_KERNEL" ] || [ ! -f "$ROOTFS" ]; then
        echo "Error: Package must contain both kernel and root payloads"
        exit 1
    fi

    for unsupported_chain_key in ota_intermediate_cert ota_revoked_intermediates; do
        if [ "$(manifest_key_count "$PACKAGE_MANIFEST" "$unsupported_chain_key")" != "0" ]; then
            echo "Error: Package profile '$PACKAGE_MANIFEST_PROFILE' forbids '$unsupported_chain_key'; certificate validity has no trusted-time authority on Zynq"
            exit 1
        fi
    done

    case "$PACKAGE_MANIFEST_PROFILE" in
        dcentos.sysupgrade-authority/v1)
            [ "$PACKAGE_STATUS" != "lab_unsigned" ] || {
                echo "Error: Package authority-v1 forbids status=lab_unsigned"
                exit 1
            }
            [ "$(manifest_key_count "$PACKAGE_MANIFEST" verification_key)" = "1" ] || {
                echo "Error: Package authority-v1 must contain exactly one verification_key payload"
                exit 1
            }
            [ -f "$PACKAGE_SIG" ] || {
                echo "Error: Package authority-v1 is missing MANIFEST.sig"
                exit 1
            }
            [ -f "$PACKAGE_RELEASE_KEY" ] || {
                echo "Error: Package authority-v1 is missing release_ed25519.pub"
                exit 1
            }
            ;;
        dcentos.sysupgrade-unsigned-lab/v1)
            [ "$ALLOW_LAB_UNSIGNED" = "1" ] || {
                echo "Error: Package unsigned-lab/v1 requires DCENT_ALLOW_UNSIGNED_SYSUPGRADE=1"
                exit 1
            }
            [ "$PACKAGE_STATUS" = "lab_unsigned" ] || {
                echo "Error: Package unsigned-lab/v1 requires exactly one status=lab_unsigned field"
                exit 1
            }
            [ "$(manifest_key_count "$PACKAGE_MANIFEST" verification_key)" = "0" ] || {
                echo "Error: Package unsigned-lab/v1 forbids a verification_key payload"
                exit 1
            }
            [ ! -e "$PACKAGE_SIG" ] || {
                echo "Error: Package unsigned-lab/v1 forbids MANIFEST.sig"
                exit 1
            }
            [ ! -e "$PACKAGE_RELEASE_KEY" ] || {
                echo "Error: Package unsigned-lab/v1 forbids release_ed25519.pub"
                exit 1
            }
            ;;
        *)
            echo "Error: Package manifest profile '$PACKAGE_MANIFEST_PROFILE' is unsupported"
            exit 1
            ;;
    esac

    if [ -f "$PACKAGE_SIG" ]; then
        if ! command -v openssl >/dev/null 2>&1; then
            echo "Error: openssl is required to verify MANIFEST.sig"
            exit 1
        fi
        if [ ! -f "$RELEASE_PUBKEY" ]; then
            echo "Error: release public key not found at $RELEASE_PUBKEY"
            exit 1
        fi
        PACKAGE_KEY_SHA=$(sha256sum "$PACKAGE_RELEASE_KEY" | awk '{print $1}')
        RELEASE_KEY_SHA=$(sha256sum "$RELEASE_PUBKEY" | awk '{print $1}')
        if [ "$PACKAGE_KEY_SHA" != "$RELEASE_KEY_SHA" ]; then
            echo "Error: Package release_ed25519.pub does not match embedded release key"
            exit 1
        fi
        if ! openssl pkeyutl -verify -rawin -pubin -inkey "$RELEASE_PUBKEY" -sigfile "$PACKAGE_SIG" -in "$PACKAGE_MANIFEST" >/dev/null 2>&1; then
            echo "Error: Package signature verification failed"
            exit 1
        fi
        echo "  Verified MANIFEST.sig against embedded release key"
    else
        echo "  WARNING: Unsigned package allowed by DCENT_ALLOW_UNSIGNED_SYSUPGRADE=1"
    fi

    enforce_sysupgrade_version_floor || exit 1

    ROOTFS_SHA=$(manifest_field "$PACKAGE_MANIFEST" payloads.rootfs.sha256 2>/dev/null || true)
    KERNEL_SHA=$(manifest_field "$PACKAGE_MANIFEST" payloads.kernel.sha256 2>/dev/null || true)
    RELEASE_KEY_SHA_EXPECTED=$(manifest_field "$PACKAGE_MANIFEST" payloads.verification_key.sha256 2>/dev/null || true)
    for authority_key in schema manifest_profile product package_type installable artifact_maturity board board_target version; do
        [ "$(manifest_key_count "$PACKAGE_MANIFEST" "$authority_key")" = "1" ] || {
            echo "Error: Package manifest must contain exactly one '$authority_key' authority field"
            exit 1
        }
    done
    for payload_key in kernel rootfs metadata; do
        [ "$(manifest_key_count "$PACKAGE_MANIFEST" "$payload_key")" = "1" ] || {
            echo "Error: Package manifest must contain exactly one '$payload_key' payload"
            exit 1
        }
    done
    PACKAGE_BITSTREAM="$PACKAGE_SUBDIR/fpga_bitstream.bit"
    PACKAGE_BITSTREAM_COUNT=$(manifest_key_count "$PACKAGE_MANIFEST" bitstream)
    if [ -e "$PACKAGE_BITSTREAM" ] || [ -L "$PACKAGE_BITSTREAM" ]; then
        [ "$PACKAGE_BITSTREAM_COUNT" = "1" ] || {
            echo "Error: Package contains fpga_bitstream.bit without exactly one signed bitstream declaration"
            exit 1
        }
    else
        [ "$PACKAGE_BITSTREAM_COUNT" = "0" ] || {
            echo "Error: Package manifest declares bitstream but fpga_bitstream.bit is absent"
            exit 1
        }
    fi
    PACKAGE_SCHEMA=$(manifest_integer_field "$PACKAGE_MANIFEST" schema 2>/dev/null || true)
    PACKAGE_MANIFEST_PROFILE=$(manifest_field "$PACKAGE_MANIFEST" manifest_profile 2>/dev/null || true)
    PACKAGE_PRODUCT=$(manifest_field "$PACKAGE_MANIFEST" product 2>/dev/null || true)
    PACKAGE_TYPE=$(manifest_field "$PACKAGE_MANIFEST" package_type 2>/dev/null || true)
    PACKAGE_INSTALLABLE=$(manifest_boolean_field "$PACKAGE_MANIFEST" installable 2>/dev/null || true)
    PACKAGE_ARTIFACT_MATURITY=$(manifest_field "$PACKAGE_MANIFEST" artifact_maturity 2>/dev/null || true)
    PACKAGE_BOARD=$(manifest_field "$PACKAGE_MANIFEST" board 2>/dev/null || true)
    PACKAGE_BOARD_TARGET=$(manifest_field "$PACKAGE_MANIFEST" board_target 2>/dev/null || true)

    [ "$PACKAGE_SCHEMA" = "1" ] || {
        echo "Error: Package manifest schema must be integer 1"
        exit 1
    }
    case "$PACKAGE_MANIFEST_PROFILE" in
        dcentos.sysupgrade-authority/v1|dcentos.sysupgrade-unsigned-lab/v1) ;;
        *)
            echo "Error: Package manifest lacks a supported sysupgrade mutation profile"
            exit 1
            ;;
    esac
    [ "$PACKAGE_PRODUCT" = "DCENT_OS" ] || {
        echo "Error: Package manifest product must be DCENT_OS"
        exit 1
    }
    [ "$PACKAGE_TYPE" = "sysupgrade" ] || {
        echo "Error: Package manifest package_type must be sysupgrade"
        exit 1
    }
    [ "$PACKAGE_INSTALLABLE" = "true" ] || {
        echo "Error: Package manifest must explicitly declare installable=true"
        exit 1
    }
    [ "$PACKAGE_ARTIFACT_MATURITY" = "experimental" ] || {
        echo "Error: Package manifest artifact_maturity must match this target's experimental policy"
        exit 1
    }
    [ -n "$PACKAGE_BOARD" ] && [ -n "$PACKAGE_BOARD_TARGET" ] && [ "$PACKAGE_BOARD" = "$PACKAGE_BOARD_TARGET" ] || {
        echo "Error: Package manifest board and board_target must be present and identical"
        exit 1
    }
    [ "$PACKAGE_SUBDIR_NAME" = "sysupgrade-$PACKAGE_BOARD_TARGET" ] || {
        echo "Error: Package directory '$PACKAGE_SUBDIR_NAME' does not match signed target 'sysupgrade-$PACKAGE_BOARD_TARGET'"
        exit 1
    }

    validate_manifest_payload_binding kernel kernel "$PACKAGE_KERNEL" || exit 1
    validate_manifest_payload_binding rootfs root "$ROOTFS" || exit 1
    validate_manifest_payload_binding metadata METADATA "$PACKAGE_SUBDIR/METADATA" || exit 1
    if [ "$PACKAGE_MANIFEST_PROFILE" = "dcentos.sysupgrade-authority/v1" ]; then
        validate_manifest_payload_binding verification_key release_ed25519.pub "$PACKAGE_RELEASE_KEY" || exit 1
    fi
    if [ "$PACKAGE_BITSTREAM_COUNT" = "1" ]; then
        validate_manifest_payload_binding bitstream fpga_bitstream.bit "$PACKAGE_BITSTREAM" || exit 1
    fi

    if [ -f "$PACKAGE_SIG" ] && [ -z "$RELEASE_KEY_SHA_EXPECTED" ]; then
        echo "Error: Signed package manifest is missing verification key hash"
        exit 1
    fi
    if [ -z "$ROOTFS_SHA" ] || [ -z "$KERNEL_SHA" ]; then
        echo "Error: Package manifest is missing required payload hashes"
        exit 1
    fi
    if [ -n "$PACKAGE_BOARD" ] && [ "$PACKAGE_BOARD" != "$EXPECTED_BOARD" ]; then
        echo "Error: Package targets '$PACKAGE_BOARD' but this unit is '$EXPECTED_BOARD'"
        exit 1
    fi
    if ! verify_sha256 "$ROOTFS" "$ROOTFS_SHA"; then
        echo "Error: Rootfs hash does not match package manifest"
        exit 1
    fi
    if ! verify_sha256 "$PACKAGE_KERNEL" "$KERNEL_SHA"; then
        echo "Error: Kernel hash does not match package manifest"
        exit 1
    fi
    if [ -f "$PACKAGE_SIG" ] && ! verify_sha256 "$PACKAGE_RELEASE_KEY" "$RELEASE_KEY_SHA_EXPECTED"; then
        echo "Error: Package release_ed25519.pub does not match signed manifest"
        exit 1
    fi

    verify_and_close_sysupgrade_input || exit 1
    echo "  Verified stable package manifest for board: ${PACKAGE_BOARD:-unknown}"
elif [ "$ALLOW_LAB_UNSIGNED" != "1" ]; then
    echo "Error: Raw squashfs sysupgrade is blocked by default."
    echo "Use a packaged sysupgrade tar, or set DCENT_ALLOW_UNSIGNED_SYSUPGRADE=1 for controlled lab recovery."
    exit 1
elif is_release_status "${DCENT_PACKAGE_STATUS:-release}"; then
    echo "Error: Raw squashfs lab sysupgrade requires DCENT_PACKAGE_STATUS to be a non-release lab value."
    echo "DCENT_ALLOW_UNSIGNED_SYSUPGRADE=1 alone is not enough."
    exit 1
else
    echo "WARNING: Raw squashfs sysupgrade allowed by DCENT_ALLOW_UNSIGNED_SYSUPGRADE=1"
fi

# Verify squashfs magic
MAGIC=$(hexdump -n 4 -e '4/1 "%02x"' "$ROOTFS" 2>/dev/null)
if [ "$MAGIC" != "68737173" ]; then
    echo "Error: rootfs payload from $ROOTFS_ORIGINAL does not appear to be a squashfs image"
    echo "Expected magic: hsqs, got: $MAGIC"
    exit 1
fi

if [ "$PACKAGE_INPUT_IS_TAR" = 1 ]; then
    ROOTFS_SIZE=$(wc -c < "$ROOTFS")
else
    ROOTFS_SIZE=$DCENT_SYSUPGRADE_INPUT_SIZE
fi
if ! dcent_zynq_geometry_require_payload_fit "$EXPECTED_BOARD" rootfs "$ROOTFS_SIZE"; then
    echo "Error: rootfs payload exceeds the evidence-backed package window."
    exit 1
fi
if [ -n "$PACKAGE_KERNEL" ]; then
    PACKAGE_KERNEL_SIZE=$(wc -c < "$PACKAGE_KERNEL" | tr -d '[:space:]')
    if ! dcent_zynq_geometry_require_payload_fit "$EXPECTED_BOARD" kernel "$PACKAGE_KERNEL_SIZE"; then
        echo "Error: package kernel exceeds the evidence-backed package window."
        exit 1
    fi
fi

echo "============================================"
echo "  DCENTos Self-Update (A/B Slot) — am2-s17plus"
echo "============================================"
echo ""
echo "  Board:            $EXPECTED_BOARD (S17+ Zynq)"
echo "  Current firmware: $CURRENT_FW (mtd$CURRENT_MTD) — running"
echo "  Target firmware:  $INACTIVE_FW (mtd$INACTIVE_MTD) — will be updated"
echo ""
echo "  Upgrade input: $ROOTFS_ORIGINAL"
echo "  Rootfs payload: $ROOTFS ($ROOTFS_SIZE bytes)"
if [ -n "$PACKAGE_MANIFEST" ]; then
    echo "  Package manifest: $PACKAGE_MANIFEST"
fi
echo ""
echo "  Expected inactive UBI layout (LEBs):"
echo "    kernel=$EXPECTED_KERNEL_LEBS  rootfs=$EXPECTED_ROOTFS_LEBS  rootfs_data=$EXPECTED_ROOTFS_DATA_LEBS"
echo ""

[ -r "$AM2_OTA_HELPER" ] || { echo 'Error: native AM2 OTA participant is missing' >&2; exit 1; }
. "$AM2_OTA_HELPER"
am2_ota_validate_package || { echo 'Error: native AM2 signed FPGA payload validation failed' >&2; exit 1; }

if [ "$TEST_ONLY" = "1" ]; then
    verify_and_close_sysupgrade_input || exit 1
    echo "Verification succeeded — test mode, no NAND writes performed."
    exit 0
fi

if [ "$DRY_RUN" = "1" ]; then
    echo "  DRY-RUN: NAND writes and U-Boot env flip will be SKIPPED."
    echo ""
fi

if [ "${DCENT_SYSUPGRADE_OFFLINE_HARNESS:-0}" != "1" ]; then
    [ -r "$DEPLOY_UPGRADE_GUARD" ] || {
        echo "Error: deploy upgrade guard is missing: $DEPLOY_UPGRADE_GUARD"
        exit 1
    }
    . "$DEPLOY_UPGRADE_GUARD"
    command -v dcent_deploy_upgrade_guard >/dev/null 2>&1         && dcent_deploy_upgrade_guard || exit 1
fi

if [ "$FORCE" -eq 0 ] && [ "$DRY_RUN" = "0" ]; then
    echo -n "  Continue? [y/N] "
    read -r confirm
    case "$confirm" in
        y|Y|yes|YES) ;;
        *) echo "Aborted."; exit 0 ;;
    esac
elif [ "$FORCE" -eq 1 ]; then
    echo "  Force mode (-f): skipping confirmation"
fi

echo ""

# Hold the update exclusion while S82 stops the managed daemon and proves
# successful software SafeOff. The session admission prevents readmission.
if [ "$DRY_RUN" = "0" ]; then
    dcent_persist_preflight "$PERSIST_SOURCE_ROOT" || {
        echo "Error: active persistent state failed preflight validation."
        echo "  Refusing before any inactive-slot mutation."
        exit 1
    }
    if [ "$STOP_MINER" = 1 ]; then
        /etc/init.d/S82dcentrald stop || {
            echo "Error: managed daemon stop did not establish successful software SafeOff." >&2
            exit 1
        }
    fi
    /bin/sh "$SESSION_LATCH_HELPER" admit-update \
        "$PERSIST_SOURCE_ROOT" "$SYSUPGRADE_LOCK_DIR" "$CURRENT_MTD" || {
        echo "Error: a stopped daemon with successful software SafeOff is required."
        echo "  Use --stop-miner for a managed stop; failed or crashed sessions require diagnosis."
        exit 1
    }
    if ! dcent_zynq_uboot_env_admit \
        "$FW_ENV_CONFIG" "$UBOOT_ENV_PROC_MTD" \
        "$UBOOT_ENV_SYSFS_MTD_ROOT" "$UBOOT_ENV_MTD4_DEVICE"; then
        echo "Error: canonical U-Boot environment identity admission failed."
        echo "  Refusing before any inactive-slot mutation."
        exit 1
    fi
fi

# --- Step 1: Attach inactive MTD as UBI (pre-flight + write target) ---
# Authenticate native FPGA and complete boot tuple before any inactive write.
am2_ota_prepare || { echo 'Error: native AM2 FPGA/boot preparation failed' >&2; exit 1; }

echo "[1/5] Attaching inactive slot (mtd$INACTIVE_MTD) geometry probe..."

if [ "$PACKAGE_INPUT_IS_TAR" -eq 0 ]; then
    verify_sysupgrade_input_unchanged || exit 1
fi

# A pre-existing attachment is not owned by this transaction.  Refuse it;
# never adopt it and never destroy it to make the desired number available.
if ! dcent_ubi_attachment_require_absent /sys/class/ubi 1 "$INACTIVE_MTD"; then
    echo "Error: inactive MTD or requested ubi1 number is already attached."
    echo "  Refusing to detach an unowned UBI resource; reboot or reconcile the retained transaction receipt."
    exit 1
fi

# Attach as UBI device number 1 (ubi0 is the running slot)
if ! dcent_ubi_attach_mtd "$INACTIVE_MTD" 1; then
    echo "Error: inactive mtd$INACTIVE_MTD did not attach with the required 2048-byte VID offset."
    echo "  No boot environment commit occurred. Retain the recovery backup and"
    echo "  inspect the attachment diagnostics before provisioning this slot."
    exit 1
fi

# Admit the exact inactive UBI device-control node before volume control.
if ! dcent_ubi_device_admit 1; then
    echo "Error: inactive UBI device node failed exact sysfs-backed admission."
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi
for volume_index in 0 1 2; do
    if [ -r "/sys/class/ubi/ubi1_${volume_index}/dev" ]; then
        dcent_ubi_volume_admit 1 "$volume_index" || {
            echo "Error: inactive UBI volume $volume_index failed exact node admission."
            dcent_ubi_detach_device 1 2>/dev/null || true
            exit 1
        }
    fi
done

# Verify the expected volumes exist — OR create them on a factory-blank slot.
#
# Phase 3 Agent P0b (2026-04-20) probed .139 and found the inactive slot is a
# pristine UBI image with volumes_count=0 (factory state for an A/B slot that
# has never been flipped). Capacity is correct (412 LEBs = 23+179+210). In
# that state we must call `ubimkvol` to create kernel/rootfs/rootfs_data
# before ubiupdatevol — mirrors the BraiinsOS sysupgrade pattern.
#
# If volumes exist, we fall through to the layout-check path below (existing
# brick-prevention gate). If volumes do NOT exist AND the capacity is right,
# we auto-create them. If neither condition holds, refuse — don't guess.
if ! VOLUMES_COUNT=$(cat /sys/class/ubi/ubi1/volumes_count 2>/dev/null); then
    echo "Error: cannot read inactive UBI volume count; refusing to infer a blank slot."
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi
case "$VOLUMES_COUNT" in
    ''|*[!0-9]*)
        echo "Error: inactive UBI volume count is not a decimal integer."
        dcent_ubi_detach_device 1 2>/dev/null || true
        exit 1
        ;;
esac
if [ ! -e /dev/ubi1_0 ] || [ ! -e /dev/ubi1_1 ]; then
    if [ "$VOLUMES_COUNT" = "0" ]; then
        if ! AVAIL_LEBS=$(cat /sys/class/ubi/ubi1/avail_eraseblocks 2>/dev/null); then
            echo "Error: cannot read inactive UBI available eraseblocks."
            dcent_ubi_detach_device 1 2>/dev/null || true
            exit 1
        fi
        case "$AVAIL_LEBS" in
            ''|*[!0-9]*)
                echo "Error: inactive UBI available eraseblocks is not a decimal integer."
                dcent_ubi_detach_device 1 2>/dev/null || true
                exit 1
                ;;
        esac
        NEEDED_LEBS=$((EXPECTED_KERNEL_LEBS + EXPECTED_ROOTFS_LEBS + EXPECTED_ROOTFS_DATA_LEBS))
        if [ "$AVAIL_LEBS" -lt "$NEEDED_LEBS" ]; then
            echo "Error: inactive slot has $AVAIL_LEBS available LEBs but template needs $NEEDED_LEBS."
            echo "  UBI provisioning differs from expected $EXPECTED_BOARD layout. Refuse."
            dcent_ubi_detach_device 1 2>/dev/null || true
            exit 1
        fi
        UBI_LEB_SIZE=$(ubi_dev_leb_size 1)
        case "$UBI_LEB_SIZE" in
            *[!0-9]*|"")
                echo "Error: cannot read inactive slot UBI logical eraseblock size."
                echo "  Refusing to create factory-blank volumes without exact geometry."
                dcent_ubi_detach_device 1 2>/dev/null || true
                exit 1
                ;;
        esac
        if [ "$DRY_RUN" = "1" ]; then
            echo "  [DRY-RUN] Would create volumes (factory-blank slot detected):"
            echo "    ubimkvol /dev/ubi1 -N kernel      -s $((EXPECTED_KERNEL_LEBS * UBI_LEB_SIZE)) -t dynamic"
            echo "    ubimkvol /dev/ubi1 -N rootfs      -s $((EXPECTED_ROOTFS_LEBS * UBI_LEB_SIZE)) -t dynamic"
            echo "    ubimkvol /dev/ubi1 -N rootfs_data -s $((EXPECTED_ROOTFS_DATA_LEBS * UBI_LEB_SIZE)) -t dynamic"
            # In dry-run with factory-blank slot, skip the reserved_ebs layout
            # check below (volumes don't exist yet to read LEB counts from).
            # Detach and report success — the real flash path will create them.
            echo ""
            echo "  [DRY-RUN] Layout check SKIPPED (volumes will be created on real flash)."
            dcent_ubi_detach_device 1 2>/dev/null || true
            echo ""
            echo "[DRY-RUN SUMMARY] All pre-flight checks passed. Tarball is flashable."
            echo "  Package:  $(basename "$PACKAGE_DIR")"
            echo "  Board:   $EXPECTED_BOARD"
            echo "  Target:  firmware$INACTIVE_FW (mtd$INACTIVE_MTD)"
            echo "  UBI:     factory-blank slot — will create kernel/rootfs/rootfs_data volumes on real flash."
            verify_and_close_sysupgrade_input || exit 1
            exit 0
        else
            echo "  Factory-blank inactive slot detected ($AVAIL_LEBS LEBs available). Creating volumes..."
            if ! dcent_ubi_make_volume 1 -N kernel -s $((EXPECTED_KERNEL_LEBS * UBI_LEB_SIZE)) -t dynamic ||
               ! dcent_ubi_volume_admit 1 0; then
                echo "Error: kernel volume creation/admission failed"; dcent_ubi_detach_device 1 2>/dev/null || true; exit 1
            fi
            if ! dcent_ubi_make_volume 1 -N rootfs -s $((EXPECTED_ROOTFS_LEBS * UBI_LEB_SIZE)) -t dynamic ||
               ! dcent_ubi_volume_admit 1 1; then
                echo "Error: rootfs volume creation/admission failed"; dcent_ubi_detach_device 1 2>/dev/null || true; exit 1
            fi
            if ! dcent_ubi_make_volume 1 -N rootfs_data -s $((EXPECTED_ROOTFS_DATA_LEBS * UBI_LEB_SIZE)) -t dynamic ||
               ! dcent_ubi_volume_admit 1 2; then
                echo "Error: rootfs_data volume creation/admission failed"; dcent_ubi_detach_device 1 2>/dev/null || true; exit 1
            fi
            echo "  Volumes created."
        fi
    else
        echo "Error: volumes_count=$VOLUMES_COUNT but ubi1_0 or ubi1_1 missing — unexpected state."
        echo "  ubi1_0: $(ls -la /dev/ubi1_0 2>/dev/null || echo 'NOT FOUND')"
        echo "  ubi1_1: $(ls -la /dev/ubi1_1 2>/dev/null || echo 'NOT FOUND')"
        dcent_ubi_detach_device 1 2>/dev/null || true
        exit 1
    fi
fi

# --- Step 1b: UBI volume layout pre-flight (CRITICAL brick-prevention) ---
# feedback_ubi_inactive_slot_volume_mismatch.md — refuse if inactive slot
# volumes don't mirror our template. This is the bulk of the am2-specific
# safety work; the rest of the script is mechanically identical to S9.
KERNEL_LEBS=$(ubi_vol_lebs 1 0)
ROOTFS_LEBS=$(ubi_vol_lebs 1 1)
ROOTFS_DATA_LEBS=$(ubi_vol_lebs 1 2)

echo ""
echo "  Inactive slot UBI layout:"
echo "    ubi1_0 (kernel)       = ${KERNEL_LEBS:-missing} LEBs (expected $EXPECTED_KERNEL_LEBS)"
echo "    ubi1_1 (rootfs)       = ${ROOTFS_LEBS:-missing} LEBs (expected $EXPECTED_ROOTFS_LEBS)"
echo "    ubi1_2 (rootfs_data)  = ${ROOTFS_DATA_LEBS:-missing} LEBs (expected $EXPECTED_ROOTFS_DATA_LEBS)"
echo ""

LAYOUT_OK=1
if [ -z "$KERNEL_LEBS" ]; then
    echo "Error: kernel volume (ubi1_0) has no reserved_ebs — UBI corruption suspected."
    LAYOUT_OK=0
else
    K_DIFF=$((KERNEL_LEBS - EXPECTED_KERNEL_LEBS))
    if [ "$K_DIFF" -lt 0 ]; then K_DIFF=$((-K_DIFF)); fi
    if [ "$K_DIFF" -gt "$KERNEL_LEB_TOLERANCE" ]; then
        echo "Error: kernel volume LEB count $KERNEL_LEBS is outside tolerance (expected $EXPECTED_KERNEL_LEBS ± $KERNEL_LEB_TOLERANCE)."
        LAYOUT_OK=0
    fi
fi
if [ -z "$ROOTFS_LEBS" ]; then
    echo "Error: rootfs volume (ubi1_1) has no reserved_ebs — UBI corruption suspected."
    LAYOUT_OK=0
elif [ "$ROOTFS_LEBS" -lt "$EXPECTED_ROOTFS_LEBS" ]; then
    echo "Error: rootfs volume LEB count $ROOTFS_LEBS is smaller than expected $EXPECTED_ROOTFS_LEBS."
    echo "  Rootfs would overflow on write — refuse."
    LAYOUT_OK=0
fi
if [ -z "$ROOTFS_DATA_LEBS" ]; then
    echo "Error: rootfs_data volume (ubi1_2) is missing — persistent state cannot survive the slot transition."
    LAYOUT_OK=0
elif [ "$ROOTFS_DATA_LEBS" -ne "$EXPECTED_ROOTFS_DATA_LEBS" ]; then
    echo "  WARN: rootfs_data LEBs $ROOTFS_DATA_LEBS differ from template $EXPECTED_ROOTFS_DATA_LEBS (operator-managed, continuing)."
fi

if [ "$LAYOUT_OK" = "0" ]; then
    echo ""
    echo "ERROR: inactive slot UBI volumes don't match expected template for $EXPECTED_BOARD ($EXPECTED_KERNEL_LEBS/$EXPECTED_ROOTFS_LEBS/$EXPECTED_ROOTFS_DATA_LEBS)."
    echo "Refuse to flash: the inactive-slot UBI layout does not match the"
    echo "expected template — this guard prevents a known brick mode."
    echo "Investigate the inactive-slot UBI layout manually before retrying."
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi
echo "  OK: inactive UBI layout matches the am2-s17plus template."

# Bind the mutable ubi1 device number to the selected inactive MTD and the
# semantic volume map immediately before entering any ubiupdatevol phase.
if ! dcent_ubi_semantic_identity_admit 1 "$INACTIVE_MTD"; then
    echo "Error: inactive UBI identity admission failed; refusing all volume writes."
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi
echo "  OK: ubi1 identity is bound to mtd$INACTIVE_MTD and kernel/rootfs/rootfs_data."

UBI_LEB_SIZE=$(ubi_dev_leb_size 1)
if ! payload_fits_ubi_volume "rootfs" "$ROOTFS_SIZE" "$ROOTFS_LEBS" "$UBI_LEB_SIZE"; then
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi
if [ -n "$PACKAGE_KERNEL" ]; then
    PACKAGE_KERNEL_SIZE=$(wc -c < "$PACKAGE_KERNEL" | tr -d '[:space:]')
    if ! payload_fits_ubi_volume "kernel" "$PACKAGE_KERNEL_SIZE" "$KERNEL_LEBS" "$UBI_LEB_SIZE"; then
        dcent_ubi_detach_device 1 2>/dev/null || true
        exit 1
    fi
fi

if [ "$DRY_RUN" = "1" ]; then
    echo ""
    echo "[DRY-RUN] Skipping kernel/rootfs writes and U-Boot env flip."
    dcent_ubi_detach_device 1 2>/dev/null || true
    echo "Dry run complete — package + layout validated for $EXPECTED_BOARD."
    verify_and_close_sysupgrade_input || exit 1
    exit 0
fi

# --- Step 2: Write kernel ---
if [ -n "$PACKAGE_KERNEL" ]; then
    echo "[2/5] Writing verified package kernel to inactive slot..."
    KERNEL_SOURCE="$PACKAGE_KERNEL"
    KERNEL_SIZE=$(wc -c < "$PACKAGE_KERNEL")
else
    echo "[2/5] Copying kernel from active slot..."
    KERNEL_SIZE=$(cat /sys/class/ubi/ubi0_0/data_bytes 2>/dev/null)
    if [ -z "$KERNEL_SIZE" ] || [ "$KERNEL_SIZE" -eq 0 ]; then
        echo "Error: Cannot determine kernel size from ubi0_0"
        dcent_ubi_detach_device 1 2>/dev/null || true
        exit 1
    fi
    if ! workspace_require_free_bytes "active-kernel scratch copy" \
        "$((KERNEL_SIZE + SYSUPGRADE_TAR_SLACK_BYTES))"; then
        dcent_ubi_detach_device 1 2>/dev/null || true
        exit 1
    fi
    if ! dcent_sysupgrade_workspace_require_absent "$KERNEL_TEMP"; then
        dcent_ubi_detach_device 1 2>/dev/null || true
        exit 1
    fi
    dcent_ubi_semantic_identity_admit 0 "$CURRENT_MTD" || { echo "Error: active UBI identity changed before kernel copy"; dcent_ubi_detach_device 1 2>/dev/null || true; exit 1; }
    dcent_ubi_volume_admit 0 0 || { echo "Error: active kernel node changed before copy"; dcent_ubi_detach_device 1 2>/dev/null || true; exit 1; }
    if ! dd if=/dev/ubi0_0 of="$KERNEL_TEMP" bs=1M 2>/dev/null; then
        echo "Error: Failed to copy the active kernel from ubi0_0"
        rm -f "$KERNEL_TEMP"
        dcent_ubi_detach_device 1 2>/dev/null || true
        exit 1
    fi
    KERNEL_SOURCE=$KERNEL_TEMP
fi
KERNEL_SIZE=$(wc -c < "$KERNEL_SOURCE" | tr -d '[:space:]')
echo "  Kernel size: $KERNEL_SIZE bytes"

if ! payload_fits_ubi_volume "kernel" "$KERNEL_SIZE" "$KERNEL_LEBS" "$UBI_LEB_SIZE"; then
    rm -f "$KERNEL_TEMP"
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi

if ! dcent_ubi_update_volume 1 0 "$KERNEL_SOURCE"; then
    echo "Error: Failed to write kernel to ubi1_0"
    rm -f "$KERNEL_TEMP"
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi
echo "  Kernel written to ubi1_0"

# --- Step 3: Write rootfs ---
echo "[3/5] Writing rootfs to inactive slot ($ROOTFS_SIZE bytes)..."
if ! dcent_ubi_update_volume 1 1 "$ROOTFS"; then
    echo "Error: Failed to write rootfs to ubi1_1"
    rm -f "$KERNEL_TEMP"
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
fi
echo "  Rootfs written to ubi1_1"

# --- Step 3.25: Read back written payloads before any U-Boot env flip ---
preflip_fail() {
    echo "Error: $1"
    echo "  U-Boot env NOT flipped; firmware still points at active firmware=$CURRENT_FW."
    rm -f "$KERNEL_TEMP"
    umount "$PERSIST_MOUNT_ROOT" 2>/dev/null || true
    rmdir "$PERSIST_MOUNT_ROOT" 2>/dev/null || true
    dcent_ubi_detach_device 1 2>/dev/null || true
    exit 1
}

persist_mount_has_mode() {
    _required_source=$1
    _required_target=$2
    _required_mode=$3
    awk -v required_source="$_required_source" \
        -v required_target="$_required_target" \
        -v required_mode="$_required_mode" '
        $1 == required_source && $2 == required_target && $3 == "ubifs" {
            count = split($4, options, ",")
            for (i = 1; i <= count; i++) {
                if (options[i] == required_mode) found = 1
            }
        }
        END { exit found ? 0 : 1 }
    ' "$PROC_MOUNTS_PATH"
}

preflip_readback_hash() {
    _dev="$1"
    _size="$2"
    _blocks=$(( (_size + 1048575) / 1048576 ))
    [ "$_blocks" -gt 0 ] || _blocks=1
    _hash=$(dd if="$_dev" bs=1048576 count="$_blocks" 2>/dev/null \
        | head -c "$_size" | sha256sum | awk '{print $1}')
    _rc=$?
    [ "$_rc" -eq 0 ] && [ -n "$_hash" ] || return 1
    printf '%s\n' "$_hash"
}

preflip_verify_volume() {
    _label="$1"
    _dev="$2"
    _source="$3"
    _size=$(wc -c < "$_source" | tr -d '[:space:]')
    [ -n "$_size" ] && [ "$_size" -gt 0 ] || preflip_fail "pre-flip readback cannot determine $_label payload size; env NOT flipped."
    _expected=$(sha256sum "$_source" | awk '{print $1}')
    [ -n "$_expected" ] || preflip_fail "pre-flip readback cannot hash $_label source; env NOT flipped."
    _actual=$(preflip_readback_hash "$_dev" "$_size") || preflip_fail "pre-flip readback failed for $_label from $_dev; env NOT flipped."
    if [ "$_actual" != "$_expected" ]; then
        preflip_fail "pre-flip readback mismatch for $_label on $_dev; env NOT flipped."
    fi
    echo "  ${_label}: pre-flip readback SHA256 verified"
}

echo "[3.25/5] Verifying inactive slot readback before U-Boot env flip..."
dcent_ubi_semantic_identity_admit 1 "$INACTIVE_MTD" || preflip_fail "inactive UBI identity changed before kernel readback."
dcent_ubi_volume_admit 1 0 || preflip_fail "kernel volume node changed before readback."
preflip_verify_volume kernel /dev/ubi1_0 "$KERNEL_SOURCE"
dcent_ubi_semantic_identity_admit 1 "$INACTIVE_MTD" || preflip_fail "inactive UBI identity changed before rootfs readback."
dcent_ubi_volume_admit 1 1 || preflip_fail "rootfs volume node changed before readback."
preflip_verify_volume rootfs /dev/ubi1_1 "$ROOTFS"
verify_and_close_sysupgrade_input || preflip_fail "sysupgrade input stability checkpoint failed after inactive-slot readback."
rm -f "$KERNEL_TEMP"
echo "  Pre-flip readback verified; U-Boot env still points at firmware=$CURRENT_FW"
am2_ota_write_fabric || preflip_fail "inactive FPGA write/readback or complete boot rollback custody failed."

# --- Step 3.5: Durably stage persistent data before any slot flip ---
echo "[3.5/5] Staging and verifying persistent data on inactive slot..."

[ -r "$PROC_MOUNTS_PATH" ] || preflip_fail "cannot read the mount table used for persistent-state admission."
persist_mount_has_mode ubi0:rootfs_data "$PERSIST_SOURCE_ROOT" rw || \
    preflip_fail "persistent-state source is not the exact active writable ubi0:rootfs_data UBIFS mount."
[ -e /sys/class/ubi/ubi1_2 ] || \
    preflip_fail "inactive rootfs_data volume is missing; persistent state cannot be carried across the slot transition."

dcent_sysupgrade_workspace_require_absent "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "inactive data mountpoint already exists."
mkdir -m 700 "$PERSIST_MOUNT_ROOT" || preflip_fail "cannot create the inactive data mountpoint."
dcent_ubi_semantic_identity_admit 1 "$INACTIVE_MTD" || preflip_fail "inactive UBI identity changed before writable mount."
dcent_ubi_volume_admit 1 2 || preflip_fail "inactive rootfs_data node changed before writable mount."
mount -t ubifs -o rw ubi1:rootfs_data "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "cannot mount inactive rootfs_data read-write."
persist_mount_has_mode ubi1:rootfs_data "$PERSIST_MOUNT_ROOT" rw || \
    preflip_fail "inactive rootfs_data did not appear as the expected writable UBIFS mount."
dcent_persist_stage "$PERSIST_SOURCE_ROOT" "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "persistent-state staging failed."
sync || preflip_fail "sync failed after persistent-state staging."
dcent_persist_verify "$PERSIST_SOURCE_ROOT" "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "writable-mount persistent-state verification failed."
umount "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "cannot unmount inactive rootfs_data after the writable verification."

dcent_ubi_semantic_identity_admit 1 "$INACTIVE_MTD" || preflip_fail "inactive UBI identity changed before read-only remount."
dcent_ubi_volume_admit 1 2 || preflip_fail "inactive rootfs_data node changed before read-only remount."
mount -t ubifs -o ro ubi1:rootfs_data "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "cannot remount inactive rootfs_data read-only."
persist_mount_has_mode ubi1:rootfs_data "$PERSIST_MOUNT_ROOT" ro || \
    preflip_fail "inactive rootfs_data did not appear as the expected read-only UBIFS mount."
dcent_persist_verify "$PERSIST_SOURCE_ROOT" "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "read-only remount persistent-state verification failed."
umount "$PERSIST_MOUNT_ROOT" || \
    preflip_fail "cannot unmount inactive rootfs_data after the read-only verification."
rmdir "$PERSIST_MOUNT_ROOT" || preflip_fail "cannot remove the inactive data mountpoint."
echo "  Persistent state is durable and verified from a read-only remount"

# --- Step 4: Detach and flip the U-Boot boot-selector via fw_setenv ---
echo "[4/5] Switching U-Boot to firmware $INACTIVE_FW..."

dcent_ubi_semantic_identity_admit 1 "$INACTIVE_MTD" || preflip_fail "inactive UBI identity changed before final detach."
dcent_ubi_detach_device 1 || preflip_fail "inactive UBI detach failed after persistence verification."

# NOTE: we do NOT invoke `bos firmware upgrade` here — BraiinsOS ships 8 opkg
# usign keys and will reject unsigned tarballs. We write the inactive mtd
# directly (above) and flip firmware= + upgrade_stage=0 in the U-Boot env.
#
# ENV-FLIP MECHANISM — fw_setenv (libubootenv), NOT raw dd/flash_erase/
# nandwrite. EMPIRICALLY MANDATED on this am2 control board: the prior
# `dd /dev/mtd4` capture → switch_firmware CRC-rebuild → `flash_erase
# /dev/mtd4 0 0` (erases BOTH redundant env copies) → `nandwrite` path
# corrupted the U-Boot env TWICE on the live "XIL" am2 unit (Zynq 7007S,
# Micron MT29F2G08, pl35x-nand, dmesg "ECC too weak"). Root cause: raw
# whole-partition erase-then-rewrite has a zero-valid-copy window AND
# ECC-blind raw `dd` reads. The proven fix: libubootenv's `fw_setenv`
# implements U-Boot's redundant-env protocol — it writes the INACTIVE copy
# then atomically flips the 1-byte obsolete/active flag — never a
# zero-valid-copy window; ECC/bad-block aware via the MTD layer; reads
# exactly as U-Boot does. This mirrors the proven am2-s19jpro variant
# byte-for-byte (same Zynq 7007S control board, same mtd4 redundant pair).
# Our own S99upgrade (board/zynq/rootfs-overlay/etc/init.d/S99upgrade)
# records the same lesson: "libubootenv is mandatory in defconfig. The
# python+nandwrite fallback raced with U-Boot env redundancy and caused
# brick-back on .139." /etc/fw_env.config maps the SAME mtd4 redundant pair
# U-Boot reads (/dev/mtd4 0x0 0x20000 0x20000 + /dev/mtd4 0x20000 0x20000
# 0x20000).
#
# CODE-ONLY, LIVE-UNPROVEN: there is no live 17-series unit on the fleet. The
# A/B logic + mtd4 env layout are inherited verbatim from the .139-proven
# am2-s19j path (same control board); this fw_setenv flip mechanism is the
# proven-good am2-s19jpro model. Both must be live-verified before flashing
# a S17+ (see the board pin / LEB-template caveats at the top of this file).
#
# The native AM2 dispatcher programs the authenticated slot-specific FPGA
# before reading its kernel. It records the pending attempt and tries the
# previous native slot once on a failed load, returned bootm or uncommitted
# second boot. The old vendor upgrade_stage behavior is not relied upon.
# Software fixture execution verifies these commands; physical cold boot and
# power-loss behavior still require qualified tester hardware evidence.
#
# The transaction replaces the complete authenticated boot environment plus
# firmware/first_boot/upgrade_stage, preserving unrelated factory keys. The
# full receipt is copied to both data volumes before this final environment
# commit. A three-selector-only repair cannot restore the boot commands.

# Pre-flight assertions — fail-closed if ANY fails. We do NOT fall back to
# raw dd/flash_erase/nandwrite: that is the BANNED root cause that bricked
# the am2 control board twice. A missing/wrong fw_setenv environment is a
# hard stop with NO env mutation attempted (the inactive slot is already
# written; leaving the boot-selector untouched keeps the unit on its
# still-good ACTIVE slot).
_recovery_hint() {
    echo "  Recovery: the inactive slot was written but the U-Boot"
    echo "  boot-selector was NOT changed — this unit still boots its"
    echo "  current/ACTIVE slot (firmware=$CURRENT_FW). It is SAFE to"
    echo "  power-cycle (no flip was committed). To retry, restore"
    echo "  libubootenv-tools + /etc/fw_env.config and re-run sysupgrade."
}
if ! command -v fw_setenv >/dev/null 2>&1; then
    echo "Error: fw_setenv not found — libubootenv-tools missing."
    echo "  REFUSING to fall back to raw dd/flash_erase/nandwrite (that"
    echo "  ECC-blind erase-both-then-rewrite path bricked this am2"
    echo "  control board twice). No U-Boot env change attempted."
    _recovery_hint
    exit 1
fi
if ! command -v fw_printenv >/dev/null 2>&1; then
    echo "Error: fw_printenv not found — cannot verify the env flip."
    echo "  REFUSING to flip blind (no readback oracle). No U-Boot env"
    echo "  change attempted."
    _recovery_hint
    exit 1
fi
if ! dcent_zynq_uboot_env_admit \
    "$FW_ENV_CONFIG" "$UBOOT_ENV_PROC_MTD" \
    "$UBOOT_ENV_SYSFS_MTD_ROOT" "$UBOOT_ENV_MTD4_DEVICE"; then
    echo "Error: canonical U-Boot environment identity changed before env access."
    echo "  No U-Boot env change attempted."
    _recovery_hint
    exit 1
fi
# Sanity: fw_printenv must read a CRC-valid env now (no "Bad CRC"/"using
# default environment") AND its current firmware= must equal the running
# slot ($CURRENT_FW). If the live env is unreadable or inconsistent with the
# running system, we do NOT mutate it (a corrupt/foreign env + a write = brick).
_PRECHK_RC=0
_PRECHK=$(fw_printenv -c "$FW_ENV_CONFIG" 2>&1) || _PRECHK_RC=$?
if [ "$_PRECHK_RC" -ne 0 ]; then
    echo "Error: fw_printenv failed while reading the current U-Boot env."
    echo "  Refusing to mutate an env that was not read successfully."
    _recovery_hint
    exit 1
fi
case "$_PRECHK" in
    *"Bad CRC"*|*"using default environment"*)
        echo "Error: fw_printenv reports an INVALID current U-Boot env"
        echo "  (Bad CRC / default environment). Refusing to fw_setenv"
        echo "  into an unreadable env. No U-Boot env change attempted."
        _recovery_hint
        exit 1
        ;;
esac
_CUR_FW_NOW=$(printf '%s\n' "$_PRECHK" | sed -n 's/^firmware=//p')
_CUR_STG_NOW=$(printf '%s\n' "$_PRECHK" | sed -n 's/^upgrade_stage=//p')
_CUR_FIRST_BOOT_NOW=$(printf '%s\n' "$_PRECHK" | sed -n 's/^first_boot=//p')
_CUR_FW_COUNT=$(printf '%s\n' "$_PRECHK" | sed -n 's/^firmware=//p' | wc -l | tr -d ' ')
_CUR_STG_COUNT=$(printf '%s\n' "$_PRECHK" | sed -n 's/^upgrade_stage=//p' | wc -l | tr -d ' ')
_CUR_FIRST_COUNT=$(printf '%s\n' "$_PRECHK" | sed -n 's/^first_boot=//p' | wc -l | tr -d ' ')
if [ "$_CUR_FW_COUNT" != 1 ] || [ "$_CUR_STG_COUNT" -gt 1 ] || \
   [ "$_CUR_FIRST_COUNT" -gt 1 ] || [ -z "$_CUR_FW_NOW" ]; then
    echo "Error: current boot-selector tuple is missing or duplicated."
    echo "  Expected exactly one firmware= and at most one optional"
    echo "  upgrade_stage= and first_boot= value."
    echo "  No U-Boot env change attempted."
    _recovery_hint
    exit 1
fi
if [ "$_CUR_FW_NOW" != "$CURRENT_FW" ]; then
    echo "Error: live U-Boot env firmware=$_CUR_FW_NOW does not match the"
    echo "  running slot firmware=$CURRENT_FW (from /proc/cmdline). The"
    echo "  env fw_setenv would target is inconsistent with the booted"
    echo "  system — refusing to mutate it. No U-Boot env change attempted."
    _recovery_hint
    exit 1
fi
# Forensic-only snapshot of the pre-flip env (harmless text; NOT a raw
# ECC-blind image — we never restore a raw dd image, that worsened a
# real incident). Recovery replays the complete authenticated boot environment.
dcent_sysupgrade_workspace_require_absent "$ENV_PRE_TEMP" || exit 1
printf '%s\n' "$_PRECHK" >"$ENV_PRE_TEMP" || exit 1

# THE FLIP — a single libubootenv redundant-environment transaction. An error
# return alone does not prove which complete tuple is durable, so readback and
# the armed transaction journal resolve that ambiguity.
dcent_sysupgrade_workspace_require_absent "$FW_SETENV_SCRIPT" || exit 1
am2_ota_commit_script || preflip_fail "native source environment changed before the complete boot commit."
dcent_sysupgrade_lock_arm_env_commit || {
    rm -f "$FW_SETENV_SCRIPT"
    echo "Error: could not arm the boot-environment transaction journal."
    exit 1
}
if ! am2_ota_apply_environment; then
    if [ "$AM2_OTA_ENV_RESULT" = unchanged ]; then
        dcent_sysupgrade_lock_abort_env_commit || \
            echo "WARNING: complete prior environment is intact, but journal disarm failed; same-boot retries remain blocked." >&2
        echo "Error: the environment commit did not apply. Complete CRC-valid readback"
        echo "  proves the prior environment, including all boot commands and factory keys."
        _recovery_hint
    else
        echo "Error: readback did not prove the complete prior or new boot environment."
        echo "  The armed journal is retained; do not reboot or retry this transaction."
        echo "  Use serial/SD recovery to inspect the retained full environment receipt."
        echo "  Restore the complete authenticated boot tuple through fw_setenv --script."
        echo "  Changing only firmware, upgrade_stage and first_boot is insufficient."
    fi
    exit 1
fi
rm -f "$ENV_PRE_TEMP" 2>/dev/null

# Keep the process-wide transaction lock in /run after the verified commit.
# It disappears only on reboot, preventing a second writer from entering the
# post-flip/pre-reboot interval if the reboot command is delayed or fails.
dcent_sysupgrade_lock_preserve || exit 1

echo "  U-Boot env updated: firmware=$INACTIVE_FW"

# --- Step 5: Reboot ---
echo "[5/5] Update complete! Rebooting..."
echo ""
echo "  The miner will boot into firmware $INACTIVE_FW (mtd$INACTIVE_MTD)."
echo "  The verified native boot dispatcher tries firmware $CURRENT_FW if the new boot fails."
echo "  Retain the recovery backup and the protected SSH credential for this unit."
echo ""
sleep 2
reboot
