#!/bin/sh
set -u

STEP_TAG="${STEP_TAG:-STEP_050M07R14C_VM101_OPERATIONS_TOOLSET_BASELINE_R03_REBUILT}"
EXPECTED_COMMIT="${EXPECTED_COMMIT:-0a6998cfead6a1e8b820cce9c00171dd6e81c6a1}"
EXPECTED_TREE="${EXPECTED_TREE:-b2646e97025823532daa549235e1643f75f11c4e}"
PACKAGE_FILE="${PACKAGE_FILE:-/root/router-toolset-transfer/package-list.txt}"
STATE_ROOT="${STATE_ROOT:-/root/router-ops-toolset-baseline}"
STAMP="$(date -u +%Y%m%d-%H%M%S 2>/dev/null || date +%Y%m%d-%H%M%S)"
STATE_DIR="$STATE_ROOT/${STAMP}_${STEP_TAG}"
CHANGE_STARTED=false
AUTO_ROLLBACK=NOT_NEEDED
RESULT=STOP_UNHANDLED_TARGET_ERROR
STOP_REASON=unhandled
PACKAGE_MANAGER=
NEW_REQUESTED_COUNT=0
NEW_ALL_COUNT=0

mkdir -p "$STATE_DIR" || exit 10
LOG="$STATE_DIR/main.log"
: >"$LOG"

progress() {
    line="$(printf '>>> [%s] %s' "$(date -u +%H:%M:%S 2>/dev/null || date +%H:%M:%S)" "$*")"
    printf '%s\n' "$line"
    printf '%s\n' "$line" >>"$LOG"
}

git_value() {
    git --git-dir=/root/.vm101-source.git --work-tree=/ "$@"
}

package_names_opkg() {
    opkg list-installed 2>/dev/null | awk '{print $1}' | sort -u
}

package_names_apk() {
    apk info 2>/dev/null | sort -u
}

capability_map() {
    out="$1"
    : >"$out"
    for cmd in \
        sh tar gzip base64 sha256sum awk stat timeout realpath readlink \
        sort nl diff jq curl tcpdump dig ip pgrep pkill watch \
        find sed grep cut tr head tail wc date git uci ubus
    do
        path="$(command -v "$cmd" 2>/dev/null || true)"
        if [ -n "$path" ]; then
            printf '%s\tPRESENT\t%s\n' "$cmd" "$path" >>"$out"
        else
            printf '%s\tMISSING\t-\n' "$cmd" >>"$out"
        fi
    done
}

endpoint_snapshot() {
    out="$1"
    : >"$out"
    for iface in vpn1 vpn2 vpn3 vpn4 vpn5; do
        endpoint="$(amneziawg show "$iface" endpoints 2>/dev/null | awk 'NR==1 {print $2}' || true)"
        printf '%s\t%s\n' "$iface" "$endpoint" >>"$out"
    done
}

write_rollback() {
    cat >"$STATE_DIR/rollback.sh" <<'ROLLBACK'
#!/bin/sh
set -u
HERE="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
PM="$(cat "$HERE/package-manager.txt" 2>/dev/null || true)"
BEFORE="$HERE/packages-before.names"
CURRENT="$HERE/packages-current.names"
ADDED="$HERE/packages-added-current.names"
LOG="$HERE/rollback.log"

exec >>"$LOG" 2>&1

case "$PM" in
    opkg)
        opkg list-installed 2>/dev/null | awk '{print $1}' | sort -u >"$CURRENT"
        ;;
    apk)
        apk info 2>/dev/null | sort -u >"$CURRENT"
        ;;
    *)
        echo 'RESULT=STOP_ROLLBACK_PACKAGE_MANAGER_UNKNOWN'
        exit 1
        ;;
esac

: >"$ADDED"
while IFS= read -r pkg; do
    [ -n "$pkg" ] || continue
    grep -Fxq "$pkg" "$BEFORE" 2>/dev/null || printf '%s\n' "$pkg" >>"$ADDED"
done <"$CURRENT"

if [ -s "$ADDED" ]; then
    case "$PM" in
        opkg)
            # Remove top-level packages first, then any dependencies that were absent before.
            if [ -s "$HERE/requested-installed-by-step.txt" ]; then
                while IFS= read -r pkg; do
                    [ -n "$pkg" ] || continue
                    opkg remove "$pkg" 2>/dev/null || true
                done <"$HERE/requested-installed-by-step.txt"
            fi
            opkg list-installed 2>/dev/null | awk '{print $1}' | sort -u >"$CURRENT"
            : >"$ADDED"
            while IFS= read -r pkg; do
                [ -n "$pkg" ] || continue
                grep -Fxq "$pkg" "$BEFORE" 2>/dev/null || printf '%s\n' "$pkg" >>"$ADDED"
            done <"$CURRENT"
            if [ -s "$ADDED" ]; then
                while IFS= read -r pkg; do
                    [ -n "$pkg" ] || continue
                    opkg remove "$pkg" 2>/dev/null || true
                done <"$ADDED"
            fi
            ;;
        apk)
            if [ -s "$HERE/requested-installed-by-step.txt" ]; then
                apk del $(cat "$HERE/requested-installed-by-step.txt") 2>/dev/null || true
            fi
            ;;
    esac
fi

AFTER_COMMIT="$(git --git-dir=/root/.vm101-source.git --work-tree=/ rev-parse HEAD 2>/dev/null || true)"
AFTER_TREE="$(git --git-dir=/root/.vm101-source.git --work-tree=/ rev-parse 'HEAD^{tree}' 2>/dev/null || true)"
echo "AFTER_COMMIT=$AFTER_COMMIT"
echo "AFTER_TREE=$AFTER_TREE"
echo 'RESULT=PASS_TOOLSET_ROLLBACK_ATTEMPTED'
ROLLBACK
    chmod 700 "$STATE_DIR/rollback.sh"
}

fail() {
    RESULT="$1"
    STOP_REASON="$2"
    if [ "$CHANGE_STARTED" = true ] && [ -x "$STATE_DIR/rollback.sh" ]; then
        AUTO_ROLLBACK=ATTEMPTED
        sh "$STATE_DIR/rollback.sh" || AUTO_ROLLBACK=FAILED
        [ "$AUTO_ROLLBACK" = FAILED ] || AUTO_ROLLBACK=PASS
    fi
    {
        echo "RESULT=$RESULT"
        echo "STOP_REASON=$STOP_REASON"
        echo "AUTO_ROLLBACK=$AUTO_ROLLBACK"
        echo "STATE_DIR=$STATE_DIR"
    } >"$STATE_DIR/final.txt"
    cat "$STATE_DIR/final.txt"
    cat "$STATE_DIR/final.txt" >>"$LOG"
    exit 1
}

progress "preflight exact Machine Git source"
BEFORE_COMMIT="$(git_value rev-parse HEAD 2>/dev/null || true)"
BEFORE_TREE="$(git_value rev-parse 'HEAD^{tree}' 2>/dev/null || true)"
[ "$BEFORE_COMMIT" = "$EXPECTED_COMMIT" ] || fail STOP_VM101_EXACT_PREFLIGHT_FAILED commit
[ "$BEFORE_TREE" = "$EXPECTED_TREE" ] || fail STOP_VM101_EXACT_PREFLIGHT_FAILED tree
git_value status --short >"$STATE_DIR/git-status-before.txt" 2>&1 || true
[ ! -s "$STATE_DIR/git-status-before.txt" ] || fail STOP_VM101_WORKTREE_NOT_CLEAN git_status

cat /etc/openwrt_release >"$STATE_DIR/openwrt-release.txt" 2>/dev/null || true
uname -a >"$STATE_DIR/uname.txt" 2>/dev/null || true
df -Pk / /overlay >"$STATE_DIR/df-before.txt" 2>/dev/null || df -Pk / >"$STATE_DIR/df-before.txt"
FREE_KB="$(df -Pk /overlay 2>/dev/null | awk 'NR==2 {print $4}')"
[ -n "$FREE_KB" ] || FREE_KB="$(df -Pk / | awk 'NR==2 {print $4}')"
case "$FREE_KB" in
    ''|*[!0-9]*) fail STOP_FREE_SPACE_CHECK_FAILED free_space ;;
esac
[ "$FREE_KB" -ge 32768 ] || fail STOP_INSUFFICIENT_FREE_SPACE "$FREE_KB"
printf 'FREE_KB_BEFORE=%s\n' "$FREE_KB" >"$STATE_DIR/free-space.txt"
capability_map "$STATE_DIR/capabilities-before.tsv"
endpoint_snapshot "$STATE_DIR/endpoints-before.tsv"

if command -v opkg >/dev/null 2>&1; then
    PACKAGE_MANAGER=opkg
    package_names_opkg >"$STATE_DIR/packages-before.names"
    opkg list-installed >"$STATE_DIR/packages-before.txt" 2>/dev/null || true
elif command -v apk >/dev/null 2>&1; then
    PACKAGE_MANAGER=apk
    package_names_apk >"$STATE_DIR/packages-before.names"
    apk info -vv >"$STATE_DIR/packages-before.txt" 2>/dev/null || apk info >"$STATE_DIR/packages-before.txt"
else
    fail STOP_PACKAGE_MANAGER_NOT_FOUND package_manager
fi
printf '%s\n' "$PACKAGE_MANAGER" >"$STATE_DIR/package-manager.txt"

[ -s "$PACKAGE_FILE" ] || fail STOP_PACKAGE_LIST_MISSING package_list
cp "$PACKAGE_FILE" "$STATE_DIR/requested-packages.txt"
: >"$STATE_DIR/requested-installed-by-step.txt"
write_rollback

# Package metadata update is allowed before package installation; no blind upgrade is ever performed.
progress "refresh package metadata"
case "$PACKAGE_MANAGER" in
    opkg)
        opkg update >"$STATE_DIR/package-update.log" 2>&1 || fail STOP_PACKAGE_METADATA_UPDATE_FAILED opkg_update
        ;;
    apk)
        apk update >"$STATE_DIR/package-update.log" 2>&1 || fail STOP_PACKAGE_METADATA_UPDATE_FAILED apk_update
        ;;
esac

progress "verify requested packages are available"
: >"$STATE_DIR/packages-availability.tsv"
while IFS= read -r pkg; do
    [ -n "$pkg" ] || continue
    if grep -Fxq "$pkg" "$STATE_DIR/packages-before.names"; then
        printf '%s\tALREADY_INSTALLED\n' "$pkg" >>"$STATE_DIR/packages-availability.tsv"
        continue
    fi
    case "$PACKAGE_MANAGER" in
        opkg)
            if opkg list "$pkg" 2>/dev/null | awk -v p="$pkg" '$1==p {found=1} END{exit found?0:1}'; then
                printf '%s\tAVAILABLE\n' "$pkg" >>"$STATE_DIR/packages-availability.tsv"
            else
                printf '%s\tMISSING_FROM_FEED\n' "$pkg" >>"$STATE_DIR/packages-availability.tsv"
                fail STOP_REQUIRED_PACKAGE_NOT_AVAILABLE "$pkg"
            fi
            ;;
        apk)
            if apk search -x "$pkg" 2>/dev/null | grep -q .; then
                printf '%s\tAVAILABLE\n' "$pkg" >>"$STATE_DIR/packages-availability.tsv"
            else
                printf '%s\tMISSING_FROM_FEED\n' "$pkg" >>"$STATE_DIR/packages-availability.tsv"
                fail STOP_REQUIRED_PACKAGE_NOT_AVAILABLE "$pkg"
            fi
            ;;
    esac
done <"$PACKAGE_FILE"

progress "install operations toolset"
CHANGE_STARTED=true
while IFS= read -r pkg; do
    [ -n "$pkg" ] || continue
    if grep -Fxq "$pkg" "$STATE_DIR/packages-before.names"; then
        echo "PACKAGE_SKIP_ALREADY_INSTALLED=$pkg"
        continue
    fi
    case "$PACKAGE_MANAGER" in
        opkg)
            opkg install "$pkg" >>"$STATE_DIR/package-install.log" 2>&1 || fail STOP_PACKAGE_INSTALL_FAILED "$pkg"
            ;;
        apk)
            apk add "$pkg" >>"$STATE_DIR/package-install.log" 2>&1 || fail STOP_PACKAGE_INSTALL_FAILED "$pkg"
            ;;
    esac
    printf '%s\n' "$pkg" >>"$STATE_DIR/requested-installed-by-step.txt"
done <"$PACKAGE_FILE"

case "$PACKAGE_MANAGER" in
    opkg)
        package_names_opkg >"$STATE_DIR/packages-after.names"
        opkg list-installed >"$STATE_DIR/packages-after.txt" 2>/dev/null || true
        ;;
    apk)
        package_names_apk >"$STATE_DIR/packages-after.names"
        apk info -vv >"$STATE_DIR/packages-after.txt" 2>/dev/null || apk info >"$STATE_DIR/packages-after.txt"
        ;;
esac

: >"$STATE_DIR/packages-added-all.names"
while IFS= read -r pkg; do
    [ -n "$pkg" ] || continue
    grep -Fxq "$pkg" "$STATE_DIR/packages-before.names" || printf '%s\n' "$pkg" >>"$STATE_DIR/packages-added-all.names"
done <"$STATE_DIR/packages-after.names"

NEW_REQUESTED_COUNT="$(wc -l <"$STATE_DIR/requested-installed-by-step.txt" | tr -d ' ')"
NEW_ALL_COUNT="$(wc -l <"$STATE_DIR/packages-added-all.names" | tr -d ' ')"

progress "verify R15A and operations capability contract"
capability_map "$STATE_DIR/capabilities-after.tsv"

missing=0
for cmd in \
    base64 stat timeout realpath readlink sort nl diff awk jq curl tcpdump \
    dig ip pgrep pkill watch sha256sum gzip tar find sed grep git uci
do
    command -v "$cmd" >/dev/null 2>&1 || {
        echo "MISSING_REQUIRED_COMMAND=$cmd" >>"$STATE_DIR/capability-contract.txt"
        missing=$((missing + 1))
    }
done
[ "$missing" -eq 0 ] || fail STOP_CAPABILITY_CONTRACT_FAILED missing_commands

roundtrip="$(printf 'router-ops-toolset' | base64 | base64 -d 2>/dev/null || true)"
[ "$roundtrip" = "router-ops-toolset" ] || fail STOP_CAPABILITY_CONTRACT_FAILED base64_roundtrip

stat_value="$(stat -c %Y /etc/openwrt_release 2>/dev/null || true)"
case "$stat_value" in
    ''|*[!0-9]*) fail STOP_CAPABILITY_CONTRACT_FAILED stat ;;
esac

timeout 3 sh -c 'exit 0' || fail STOP_CAPABILITY_CONTRACT_FAILED timeout
realpath /etc/openwrt_release >/dev/null 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED realpath
ln -sf /etc/openwrt_release "$STATE_DIR/readlink-test"
[ "$(readlink "$STATE_DIR/readlink-test" 2>/dev/null)" = "/etc/openwrt_release" ] || fail STOP_CAPABILITY_CONTRACT_FAILED readlink
printf 'b\na\n' | sort | grep -qx 'a' || fail STOP_CAPABILITY_CONTRACT_FAILED sort
printf 'x\n' | nl -ba | grep -q '1' || fail STOP_CAPABILITY_CONTRACT_FAILED nl
printf '{"ok":true}\n' | jq -e '.ok == true' >/dev/null || fail STOP_CAPABILITY_CONTRACT_FAILED jq
awk 'BEGIN { if (2+2 != 4) exit 1 }' || fail STOP_CAPABILITY_CONTRACT_FAILED awk
diff /etc/openwrt_release /etc/openwrt_release >/dev/null || fail STOP_CAPABILITY_CONTRACT_FAILED diff
curl --version >"$STATE_DIR/curl-version.txt" 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED curl
tcpdump --version >"$STATE_DIR/tcpdump-version.txt" 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED tcpdump
dig -v >"$STATE_DIR/dig-version.txt" 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED dig
ip -Version >"$STATE_DIR/ip-version.txt" 2>&1 || ip -V >"$STATE_DIR/ip-version.txt" 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED ip
pgrep -V >"$STATE_DIR/pgrep-version.txt" 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED pgrep
pkill -V >"$STATE_DIR/pkill-version.txt" 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED pkill
watch -v >"$STATE_DIR/watch-version.txt" 2>&1 || fail STOP_CAPABILITY_CONTRACT_FAILED watch

echo 'R15A_CAPABILITY_CONTRACT=PASS' >"$STATE_DIR/capability-contract.txt"
echo 'NO_PACKAGE_UPGRADE_COMMAND_USED=true' >>"$STATE_DIR/capability-contract.txt"

progress "verify source and runtime unchanged"
AFTER_COMMIT="$(git_value rev-parse HEAD 2>/dev/null || true)"
AFTER_TREE="$(git_value rev-parse 'HEAD^{tree}' 2>/dev/null || true)"
git_value status --short >"$STATE_DIR/git-status-after.txt" 2>&1 || true
endpoint_snapshot "$STATE_DIR/endpoints-after.tsv"
df -Pk / /overlay >"$STATE_DIR/df-after.txt" 2>/dev/null || df -Pk / >"$STATE_DIR/df-after.txt"

[ "$AFTER_COMMIT" = "$BEFORE_COMMIT" ] || fail STOP_MACHINE_GIT_SOURCE_CHANGED commit_changed
[ "$AFTER_TREE" = "$BEFORE_TREE" ] || fail STOP_MACHINE_GIT_SOURCE_CHANGED tree_changed
[ ! -s "$STATE_DIR/git-status-after.txt" ] || fail STOP_MACHINE_GIT_SOURCE_CHANGED git_status
[ "$(sha256sum "$STATE_DIR/endpoints-before.tsv" | awk '{print $1}')" = "$(sha256sum "$STATE_DIR/endpoints-after.tsv" | awk '{print $1}')" ] || fail STOP_RUNTIME_ENDPOINTS_CHANGED endpoints

{
    echo "RESULT=PASS_VM101_OPERATIONS_TOOLSET_BASELINE_R03_REBUILT"
    echo "PACKAGE_MANAGER=$PACKAGE_MANAGER"
    echo "NEW_REQUESTED_PACKAGE_COUNT=$NEW_REQUESTED_COUNT"
    echo "NEW_ALL_PACKAGE_COUNT=$NEW_ALL_COUNT"
    echo "BEFORE_COMMIT=$BEFORE_COMMIT"
    echo "AFTER_COMMIT=$AFTER_COMMIT"
    echo "BEFORE_TREE=$BEFORE_TREE"
    echo "AFTER_TREE=$AFTER_TREE"
    echo "MACHINE_GIT_SOURCE_CHANGED=false"
    echo "ENDPOINTS_CHANGED=false"
    echo "R15A_CAPABILITY_CONTRACT=PASS"
    echo "AUTO_ROLLBACK=NOT_NEEDED"
    echo "STATE_DIR=$STATE_DIR"
} >"$STATE_DIR/final.txt"
cat "$STATE_DIR/final.txt"
cat "$STATE_DIR/final.txt" >>"$LOG"

exit 0
