#!/bin/sh
#
# Spk-up - Update packages. Read the README before adding or
# modifying any code in spk!
#
# Copyright (C) SliTaz GNU/Linux - BSD License
# Author: See AUTHORS files
#
. /usr/lib/slitaz/libspk.sh

#
# Functions
#

# Help and usage
usage() {
	name=$(basename $0)
	cat << EOT

$(boldify $(gettext "Usage:")) $name [packages|--options]

$(gettext "Update packages lists and upgrade the system")

$(boldify $(gettext "Options:"))
  --list       $(gettext "Recharge the packages lists")
  --check      $(gettext "Report upgradable packages without installing")
  --add        $(gettext "Install upgrades automatically")
  --forced     $(gettext "Force recharging the lists")
  --mirror=    $(gettext "Specify a mirror to check")
  --raw        $(gettext "Plain TSV on stdout (one upgradable per line)")
  --trame      $(gettext "JSON object on stdout (--check implied)")

$(boldify $(gettext "Examples:"))
  $name package1 package2
  $name --list --forced --mirror=main

EOT
	exit 0
}

# Headers for system or packages update
up_headers() {
	newline
	boldify $(gettext "Package") \
	$(echo -n $(indent 28 $(gettext "Version"))) \
	$(echo -n $(indent 48 $(gettext "Status"))) \
	$(echo -n $(indent 68 $(gettext "Mirror")))
	separator
}

# Fetch one mirror file into the current directory. Local mirrors
# (file:// scheme or a bare absolute path) are copied — busybox wget
# rejects file:// URLs. Remote mirrors go through wget. Relies on
# $url and $mirror_local set by recharge_lists() for the loop.
mirror_fetch() {
	if [ "$mirror_local" ]; then
		cp -f "${url%/}/$1" "$1" 2>/dev/null
	else
		busybox wget $WGET_OPT $quiet "${url%/}/$1"
	fi
}

# Cheap format sanity check for a freshly downloaded list. Returns 0 when
# the file's first non-empty line matches the shape its type demands, so a
# mirror that answered 200 with an HTML error page (or a truncated body that
# happens to start mid-line) is rejected before it poisons the DB. Only the
# head is inspected — enough to tell a real list from garbage without a full
# O(n) scan of ~6000 lines. Usage: list_format_ok <file> <md5|tsv>
list_format_ok() {
	local f="$1" kind="$2" line
	# A list we intentionally kept from .bak (empty on the mirror) is fine.
	[ -s "$f" ] || return 0
	line=$(grep -m1 . "$f" 2>/dev/null)
	# Any list starting with an HTML/XML tag is an error page, never data.
	case "$line" in '<'*) return 1 ;; esac
	case "$kind" in
		md5) # "d41d8cd98f00b204e9800998ecf8427e  pkg-1.0.tazpkg"
			case "$line" in
				[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]"  "*) return 0 ;;
				*) return 1 ;;
			esac ;;
		tsv) # packages.info is TAB-separated; a real line always has one.
			case "$line" in *"	"*) return 0 ;; *) return 1 ;; esac ;;
	esac
	return 0
}

# Recharges all lists from one mirror or all mirrors
recharge_lists() {
	newline
	boldify $(gettext "Updating packages lists")
	separator
	# Snapshot mirror metadata before touching anything. If a recharge
	# trashes the DB (network glitch mid-wget, mirror serving garbage,
	# kernel oops…) the last 3 known-good states stay restorable from
	# /var/cache/spk/snapshots/.
	snapshot_db recharge_lists
	local check="$extradb/*/mirror $PKGS_DB/mirror"
	if [ "$mirror" ]; then
		check=$extradb/$mirror/mirror
		[ "$mirror" = "main" ] && check=$PKGS_DB/mirror
	fi
	for mirror in $check; do
		[ -f "$mirror" ] || continue
		# Skip local mirror (symlinked packages.info)
		[ ! "$(readlink "$(dirname "$mirror")/packages.info")" ] || continue
		# Get want a mirror name and download url
		name=$(basename $(dirname $mirror))
		url=$(cat $mirror)
		lists="packages.list packages.md5 packages.info packages.equiv \
			files-list.lzma files-list.md5 descriptions.txt extra.list"
		[ "$(dirname $mirror)" = "$PKGS_DB" ] && name="main"
		[ "$quiet" ] && quiet="-q"

		# Local mirror? Normalize the file:// scheme to a plain path so
		# mirror_fetch() copies the lists instead of trying to wget them.
		mirror_local=""
		case "$url" in
			file://*) url=${url#file://}; mirror_local=yes ;;
			/*)       mirror_local=yes ;;
		esac

		gettext "Checking mirror:"; colorize 34 " $name"
		cd $(dirname $mirror)

		# ID — fetched quietly (tiny change-detection file, every run).
		# It doubles as the reachability probe: if it can't be fetched
		# (mirror down, DNS failure, HTTP 503…) skip this mirror and keep
		# the current lists. Wiping them on a dead mirror used to leave
		# check_pkgup spewing one fgrep error per installed package.
		[ -f "ID" ] || echo "$$" > ID
		mv ID ID.bak
		if [ "$mirror_local" ]; then
			cp -f "${url%/}/ID" ID 2>/dev/null
		else
			busybox wget $WGET_OPT -q ${url%/}/ID
		fi
		if [ ! -s "ID" ]; then
			rm -f ID; mv ID.bak ID
			colorize 31 "$(gettext 'Mirror is unreachable, keeping current lists')"
			# Propose the known alternatives so the admin can switch:
			#   echo "http://..." > /var/lib/tazpkg/mirror
			if [ -s "$PKGS_DB/mirrors" ]; then
				gettext "Alternative mirrors in"; echo " $PKGS_DB/mirrors:"
				sed 's/^/  /' "$PKGS_DB/mirrors"
			fi
			continue
		fi
		debug "ID: $(cat ID)"
		debug "ID.bak: $(cat ID.bak)"
		if [ "$(cat ID)" = "$(cat ID.bak)" ] && [ ! "$forced" ]; then
			gettext "Mirror is up-to-date"; newline
			continue
		fi

		# Backup and get all lists. A list that fails to download (mirror
		# went down mid-recharge, file missing on mirror) is restored from
		# its backup so the DB never loses a list it had before.
		for list in $lists
		do
			[ -f "$list" ] && cp -f $list $list.bak
			debug "url: ${url%/}/$list"
			rm -f $list
			mirror_fetch $list
			if [ ! -s "$list" ] && [ -f "$list.bak" ]; then
				rm -f $list
				cp -f $list.bak $list
			fi
		done

		# Integrity gate. A mirror behind a captive portal / broken CDN can
		# answer 200 with an HTML error page, and a connection cut mid-fetch
		# leaves a non-empty but truncated list — both slip past the size
		# test above and poison every downstream grep. Validate the FORMAT of
		# the two lists the resolver relies on (scheme-independent: the
		# official mirror's ID is a revision counter, not md5(packages.md5),
		# so ID can't be used as the checksum). packages.md5 must open with a
		# real "32-hex  file" line; packages.info must be tab-separated. If
		# either looks wrong, roll the WHOLE set back to the .bak snapshot so
		# the generations never mix, and skip this mirror.
		if ! list_format_ok packages.md5 md5 || ! list_format_ok packages.info tsv; then
			colorize 31 "$(gettext 'Mirror served malformed lists, rolled back')"
			for list in $lists ID; do
				[ -f "$list.bak" ] && cp -f "$list.bak" "$list"
			done
			continue
		fi
	done
	separator
	# Refresh the optional SQLite index so spk-sql and the fast paths that
	# read it stay in sync with the lists we just pulled. No-op without
	# sqlite3 (the index is a pure accelerator, never required).
	command -v sqlite3 >/dev/null 2>&1 && spk-sql import >/dev/null 2>&1
}

# Repo priority: local, extras then official. Only emit a repo that
# actually carries a packages.md5 — a bare or half-registered dir (e.g.
# extra/local with no lists) would make every fgrep/grep lookup below
# spew "No such file or directory" once per scanned package. Also fixes
# the old loop that collapsed all extras down to the last one.
priority() {
	local d
	[ -f "$extradb/local/packages.$SUM" ] && echo "$extradb/local"
	for d in "$extradb"/*; do
		[ "$d" = "$extradb/local" ] && continue
		[ -f "$d/packages.$SUM" ] && echo "$d"
	done
	echo "$PKGS_DB"
}

# Types: blocked, new build or new version.
# Appends one TSV row to $pkgsup.tsv for downstream rendering:
#   pkg<TAB>cur_ver<TAB>new_ver<TAB>status<TAB>repo
# Status is one of: blocked | new-build | new-version.
up_type() {
	# Jump to next repository if pkg doesn't exist in this one.
	grep -q "^${PACKAGE}	" $dbinfo || continue

	local status new
	# Exact field-1 match, first hit only. A plain grep "^$PACKAGE\t" both
	# treated regex metacharacters in the name as wildcards (gtk+ , gcc-lib.*)
	# and, on a depot carrying two builds of one package, returned two lines
	# so $new became multi-line and broke the TSV row below.
	new=$(awk -F'	' -v p="$PACKAGE" '$1==p{print $2; exit}' "$dbinfo")

	# Blocked — whole-line fixed-string match. An unanchored "^$PACKAGE"
	# regex marked vim (and vi) blocked whenever blocked.list held vim-tiny,
	# freezing unrelated packages forever.
	if grep -qxF "$PACKAGE" "$blocked" 2>/dev/null; then
		blocked_count=$(($blocked_count + 1))
		status=blocked
	elif [ "$VERSION" = "$new" ]; then
		build_count=$(($build_count + 1))
		status=new-build
	else
		status=new-version
	fi

	# Always record the structured row (consumed by --check/--trame).
	printf '%s\t%s\t%s\t%s\t%s\n' \
		"$PACKAGE" "$VERSION" "$new" "$status" "$reponame" \
		>> $pkgsup.tsv

	# Human rendering — skipped in raw/trame so stdout stays parseable.
	if [ "$output" = human ]; then
		echo -n "$PACKAGE"
		echo -n $(indent 28 "$VERSION")
		case "$status" in
			blocked)     echo -n $(colorize 31 $(indent 48 $(gettext "Blocked"))) ;;
			new-build)   echo -n $(colorize 34 $(indent 48 $(gettext "New build"))) ;;
			new-version) echo -n $(colorize 32 $(indent 48 $(gettext "New") $new)) ;;
		esac
		indent 68 "$reponame"
	fi

	[ "$status" = blocked ] && break
	echo "$PACKAGE" >> $pkgsup
}

# Check if we have an upgrade for a package
check_pkgup() {
	unset_receipt
	if [ -f "$pkg/receipt" ]; then
		source_receipt $pkg/receipt
	else
		# Orphaned DB entry: still recorded in installed.info but its
		# receipt directory is gone (half-removed package / corrupt DB).
		# source_receipt would 'continue' the scan loop and skip it
		# silently, hiding a real upgrade — tazpkg reports these. Recover
		# the name+version straight from installed.info instead.
		local oline=$(awk -F'	' -v p="$pkg" '$1==p{print;exit}' \
			"$installed.info" 2>/dev/null)
		[ -n "$oline" ] || return 0
		PACKAGE=$(echo "$oline" | cut -f1)
		VERSION=$(echo "$oline" | cut -f2)
		EXTRAVERSION=
	fi
	localdb=$extradb/local
	# Look up the installed package's checksum by base name, accepting
	# whatever arch suffix is actually recorded (-x86_64, -arm, -any, or
	# none on i486). Reconstructing -$SLITAZ_ARCH missed noarch '-any'.
	local base="$PACKAGE-${VERSION}${EXTRAVERSION}" s f
	sum=""
	while read s f; do
		case "$f" in
			"$base".tazpkg|"$base"-*.tazpkg) sum=$s; break ;;
		esac
	done < "$installed.$SUM"

	# Skip up-to-date local packages. Guard on the list file itself, not
	# just the dir: an empty extra/local would make fgrep error out. Require
	# a non-empty sum: with sum="" (orphan with no installed.md5 line) the
	# pattern collapses to "  $PACKAGE-", a substring that matches ANY build
	# of the package and would falsely mark it up-to-date, hiding the very
	# upgrade the orphan-recovery path above is meant to surface.
	if [ -n "$sum" ] && [ -f "$localdb/packages.$SUM" ] && fgrep -q "$sum  $PACKAGE-" $localdb/packages.$SUM; then
		reponame=$(gettext "Local")
		return 0
	fi

	for repo in $(priority); do
		dbinfo=$repo/packages.info
		dbsum=$repo/packages.$SUM
		# Skip a repo whose lists are missing (never recharged, or lost to
		# a failed recharge) instead of erroring out once per package.
		[ -f "$dbinfo" ] && [ -f "$dbsum" ] || continue

		# Mirror name
		case $repo in
			$PKGS_DB) reponame=$(gettext "Official") ;;
			$localdb) reponame=$(gettext "Local") ;;
			*) reponame=$(gettext "Extra") ;;
		esac
		# Overwrite: Local has priority
		#if [ -d "$localdb" ] && fgrep -q "$sum  $PACKAGE-" $localdb/packages.$SUM; then
			#reponame=$(gettext "Local")
		#fi

		# Sum match or not ? Repos are scanned by priority (local, extra,
		# official). up_type() jumps to the next repo when the package name
		# is absent here, so the first repo we actually compare against is
		# the highest-priority one that carries the package.
		if [ -z "$sum" ] || ! fgrep -q "$sum  $PACKAGE-" $dbsum; then
			# No installed checksum (orphan) or present under a different
			# build/version → let up_type() compare by version instead of by
			# a checksum we don't have. An empty sum must NOT reach fgrep as
			# a bare "  $PACKAGE-" substring (matches any build → false
			# up-to-date).
			up_type
			break
		else
			# Installed build is current in this repo → up to date. Without
			# this break the loop fell through to lower-priority repos (e.g. a
			# package installed from Extra would then mismatch Official's
			# different build and be reported as a bogus "New version" every
			# run, reinstalling in a loop under --add).
			break
		fi
	done
}

# Pre-download one upgradable package into the cache (no install). The
# batch upgrade fetches every package up front so a mid-run network glitch
# can't leave the system half-upgraded. download() md5-verifies the file.
download_up() {
	mirrored_pkg "$1"
	[ "$mirrored" ] || return 1
	download "$(full_package "$1")" "$mirror"
}

# Log and install an upgradable package. Run after download_up has filled
# the cache: keepcache makes spk-add --forced reuse the verified copy
# instead of fetching it a second time.
install_up() {
	mirrored_pkg $pkg
	vers=$(echo "$mirrored" | awk -F'	' '{print $2}')
	mkdir -p $logdir/$pkg
	echo "$(date '+%Y-%m-%d %H:%M') : Upgrade to $vers" >> $logdir/$pkg/up.log
	# Propagate spk-add's exit status so the caller can tell a real upgrade
	# from a failed one (e.g. unavailable dependencies) instead of counting
	# every queued package as handled.
	keepcache=yes spk-add $pkg --forced
}

#
# Output mode + json helpers (kept self-contained, see spk-search).
#

output=human
[ "$raw" ] && output=raw
[ "$trame" ] && output=trame
# --trame implies --check: never mutate the system in machine-output mode.
[ "$trame" ] && check=yes

#
# --raw --add : stream the REAL system upgrade as flat TSV milestones for the
# TUI (state<TAB>step<TAB>pkg<TAB>msg, state = step|info|ok|error|done).
# Recharge the lists, scan installed packages, then upgrade each upgradable
# one via spk-add --forced --raw (its output goes to the log; we judge each
# from its own "done" line). Plain --raw without --add keeps the existing
# non-mutating upgradable report further down untouched.
#
if [ "$raw" ] && [ "$add" ]; then
	check_root
	mkdir -p "$logdir" 2>/dev/null
	printf 'step\trecharge\t-\trefreshing package lists\n'
	recharge_lists >>"$logdir/raw-up.log" 2>&1
	cd $installed
	: > $pkgsup; : > $pkgsup.tsv
	build_count=0; blocked_count=0
	for pkg in *
	do
		check_pkgup
	done
	# Plus orphaned DB entries (in installed.info, no receipt dir).
	if [ -f "$installed.info" ]; then
		while IFS='	' read pkg _rest; do
			[ -n "$pkg" ] && [ ! -d "$pkg" ] && check_pkgup
		done < "$installed.info"
	fi
	sed -i /^$/d $pkgsup
	upnb=$(cat $pkgsup | wc -l)
	printf 'info\tplan\t-\t%s upgradable\n' "$upnb"

	r_ok=0; r_fail=0
	for pkg in $(cat $pkgsup)
	do
		[ -z "$pkg" ] && continue
		printf 'step\tupgrade\t%s\tupgrading\n' "$pkg"
		mkdir -p "$logdir/$pkg"
		echo "$(date '+%Y-%m-%d %H:%M') : Upgrade (raw)" >> "$logdir/$pkg/up.log"
		# Capture spk-add's own --raw stream into the log; its final "done"
		# line tells us whether the upgrade landed (… , 0 failed).
		res=$(spk-add "$pkg" --forced --raw 2>>"$logdir/raw-up.log")
		printf '%s\n' "$res" >> "$logdir/raw-up.log"
		case "$res" in
			*", 0 failed")
				printf 'ok\tupgrade\t%s\tupgraded\n' "$pkg"
				r_ok=$((r_ok + 1)) ;;
			*)
				printf 'error\tupgrade\t%s\tfailed (see log)\n' "$pkg"
				r_fail=$((r_fail + 1)) ;;
		esac
	done
	rm -f $pkgsup && touch $pkgsup
	printf 'done\t-\t-\t%d upgraded, %d failed\n' "$r_ok" "$r_fail"
	exit 0
fi

json_escape() {
	printf '%s' "$1" | sed \
		-e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/	/\\t/g' \
		-e 's/\r/\\r/g' -e ':a;N;$!ba;s/\n/\\n/g'
}

# Render the buffered $pkgsup.tsv into the selected output format.
# Caller has already filled it via check_pkgup loops.
render_upgradable() {
	local f="$pkgsup.tsv"
	[ -f "$f" ] || : > "$f"
	case "$output" in
	raw)
		while IFS="	" read pkg cur new st repo; do
			[ -z "$pkg" ] && continue
			printf 'upgradable\t%s\t%s\t%s\t%s\t%s\n' \
				"$pkg" "$cur" "$new" "$st" "$repo"
		done < "$f"
		# Trailing summary row: total download size (bytes + human).
		local _b=$(upgrade_total_bytes)
		printf 'total_size\t%s\t%s\n' "$_b" "$(human_size $_b)"
		;;
	trame)
		printf '{"command":"up","mode":"check","upgradable":['
		local first=yes nb=0 nblocked=0
		while IFS="	" read pkg cur new st repo; do
			[ -z "$pkg" ] && continue
			if [ "$st" = blocked ]; then
				nblocked=$((nblocked + 1))
				continue
			fi
			[ "$first" ] || printf ','
			first=
			printf '{"package":"%s","from":"%s","to":"%s","reason":"%s","mirror":"%s"}' \
				"$(json_escape "$pkg")" "$(json_escape "$cur")" \
				"$(json_escape "$new")" "$st" "$(json_escape "$repo")"
			nb=$((nb + 1))
		done < "$f"
		printf '],"blocked":['
		first=yes
		while IFS="	" read pkg cur new st repo; do
			[ "$st" = blocked ] || continue
			[ "$first" ] || printf ','
			first=
			printf '"%s"' "$(json_escape "$pkg")"
		done < "$f"
		local _b=$(upgrade_total_bytes)
		printf '],"count":%s,"blocked_count":%s,"total_size_bytes":%s,"total_size":"%s"}\n' \
			"$nb" "$nblocked" "$_b" "$(human_size $_b)"
		;;
	esac
}

# Format a byte count as a human-readable size (1024-based, like the
# receipt sizes). Usage: human_size <bytes>
human_size() {
	echo "$1" | awk '{
		t=$1;
		if(t>=1073741824) printf "%.1fG\n", t/1073741824;
		else if(t>=1048576) printf "%.1fM\n", t/1048576;
		else if(t>=1024) printf "%.1fK\n", t/1024;
		else printf "%dB\n", t;
	}'
}

# Sum the packed (download) size of every package queued for upgrade and
# echo it as an integer number of bytes. Reads $pkgsup (one package name
# per line); looks up field 7 (PACKED UNPACKED) in packages.info, extras
# first then official, keeping only the packed (first) token. K/M/G are
# 1024-based; an empty size counts as 0. Used by every output mode so the
# human and machine views always agree.
upgrade_total_bytes() {
	local pkg info sz
	for pkg in $(cat $pkgsup 2>/dev/null); do
		sz=""
		for info in $extradb/*/packages.info $pkgsinfo; do
			[ -f "$info" ] || continue
			sz=$(awk -F'\t' -v p="$pkg" '$1==p{print $7; exit}' "$info")
			[ -n "$sz" ] && break
		done
		echo "${sz%% *}"
	done | awk '
		function tobytes(s,   n,u){
			n=s; u=s; gsub(/[0-9.]/,"",u); gsub(/[A-Za-z]/,"",n);
			if(n=="") return 0;
			if(u ~ /^[Gg]/) return n*1073741824;
			if(u ~ /^[Mm]/) return n*1048576;
			if(u ~ /^[Kk]/) return n*1024;
			return n;
		}
		{ t += tobytes($1) }
		END{ printf "%d\n", t }'
}

#
# Handle packages and --options
#

count=0

for arg in $@
do
	case "$arg" in
		*usage|*help) usage ;;
		--list)
			check_root
			recharge_lists
			newline && exit 0 ;;
		--*) continue ;;
		*)
			pkg="$arg"
			system=no
			check_root
			if [ "$count" = 0 ]; then
				up_headers
				# Individual query (`spk-up <pkg>`): up_type appends to
				# $pkgsup, and this path — unlike the system-wide scan — never
				# truncates it, so every run left a stale line in the
				# persistent packages.up that tazpkg-notify reads. Point
				# $pkgsup at a throwaway so the notify list stays untouched.
				pkgsup=$(mktemp 2>/dev/null || echo "${pkgsup}.q$$")
				: > "$pkgsup"; : > "$pkgsup.tsv"
			fi
			if is_package_installed $pkg; then
				count=$(($count +1))
				cd $installed
				source_receipt $pkg/receipt
				check=$(check_pkgup)
				if [ "$check" != "" ]; then
					echo "$check"
					[ "$add" ] && install_up
				else
					echo -n "$pkg"
					echo -n $(indent 28 "$VERSION")
					echo -n $(colorize 32 $(indent 48 $(gettext "up-to-date")))
					check_pkgup
					indent 68 "$reponame"
				fi
			fi ;;
	esac
done

# Skip system-wide upgrade if some packages was updated individually.
if [ "$system" ]; then
	# Drop the throwaway query list; the persistent packages.up is untouched.
	rm -f "$pkgsup" "$pkgsup.tsv"
	[ "$add" ] || newline
	exit 0
fi

#
# Check all mirrors list and upgrade system.
#

time=$(date +%s)
build_count=0
blocked_count=0

check_root
# --check audits the current state of the lists, without recharging
# from the network. This keeps the audit non-destructive (a failed
# recharge can wipe the local DB) and makes the trame integration
# safe to call from automation / TazPanel.
[ -z "$check" ] && recharge_lists
[ "$output" = human ] && up_headers
cd $installed
newline > $pkgsup
: > $pkgsup.tsv

# Check all installed packages
for pkg in *
do
	check_pkgup
done
# Plus orphaned DB entries: recorded in installed.info but with no
# receipt dir, so the glob above can't see them (check_pkgup recovers
# them from installed.info).
if [ -f "$installed.info" ]; then
	while IFS='	' read pkg _rest; do
		[ -n "$pkg" ] && [ ! -d "$pkg" ] && check_pkgup
	done < "$installed.info"
fi

# Remove empty line and count
sed -i /^$/d $pkgsup
upnb=$(cat $pkgsup | wc -l)
pkgs=$(ls | wc -l)
time=$(($(date +%s) - $time))

# --check / raw / trame: render the structured upgrade list and exit
# BEFORE the human summary / install prompt would kick in.
if [ "$check" ] || [ "$output" != human ]; then
	render_upgradable
	exit 0
fi

if [ "$upnb" = 0 ] && [ "$blocked_count" = 0 ]; then
	gettext "System is up-to-date..."; newline
fi
separator
echo -n "$pkgs "; gettext "installed packages scanned in"; echo " ${time}s"
newline

# Summary
boldify $(gettext "Packages upgrade summary")
separator
gettext "New version :"; colorize 32 " $(($upnb - $build_count))"
gettext "New build   :"; colorize 34 " $build_count"
gettext "Blocked     :"; colorize 31 " $blocked_count"
gettext "Total size  :"; colorize 34 " $(human_size $(upgrade_total_bytes))"
separator
newline

# Pkgs to upgrade ? --add/--install skip the confirm and install all.
if [ "$upnb" -gt 0 ]; then
	if [ ! "$add" ] && [ ! "$install" ]; then
		# Read the answer directly instead of libtaz confirm(): its
		# im()/`tty -s` guard mis-detects the SliTaz chroot pty (fd 0 is a
		# real /dev/pts node yet `tty -s` returns false), so confirm()
		# silently skips the read and cancels every interactive upgrade.
		# A plain read works on that same terminal; EOF (piped/cron) still
		# falls through to the safe default of not upgrading. --add stays
		# the explicit non-interactive path.
		gettext "Do you wish to upgrade now"; printf ' [y/N] ? '
		read answer
		case "$answer" in
			y|Y|yes|Yes) ;;
			*) gettext "Upgrade cancelled"; echo -e "\n"; exit 0 ;;
		esac
	fi
	# Clean the cache so the prefetch lands fresh copies.
	spk clean
	# Phase 1 — download every upgrade up front (nothing installed yet).
	# A network glitch here aborts before any package is swapped, so the
	# system never ends up half-upgraded.
	newline
	boldify $(gettext "Downloading packages")
	separator
	dl_ok=""; dl_fail=""
	for pkg in $(cat $pkgsup)
	do
		# Subshell contains check_download's hard `exit 1` on an md5
		# mismatch (e.g. a package whose file drifted from packages.md5
		# while the mirror was being regenerated). Without it, one bad
		# package would kill the whole batch before anything installs.
		# A failed download is skipped and reported; the rest proceed.
		if ( download_up "$pkg" ); then
			dl_ok="$dl_ok $pkg"
		else
			dl_fail="$dl_fail $pkg"
		fi
	done
	# Phase 2 — install from the cache. spk goes first: a newer spk may
	# understand package formats/features the rest of the batch needs (same
	# reason tazpkg upgrades itself before everything else). install_up
	# passes keepcache so --forced reuses the prefetched, verified files.
	newline
	boldify $(gettext "Installing upgrades")
	separator
	up_ok=0; up_fail=0; failed=""
	# Only install packages that actually downloaded (dl_ok). spk goes
	# first; the case guard skips spk if it wasn't in the downloaded set.
	for pkg in spk $(echo $dl_ok | tr ' ' '\n' | grep -vx spk)
	do
		case " $dl_ok " in *" $pkg "*) ;; *) continue ;; esac
		if install_up; then
			up_ok=$(($up_ok + 1))
		else
			up_fail=$(($up_fail + 1))
			failed="$failed $pkg"
		fi
	done
	# List is generated each time and must be cleaned so
	# tazpkg-notify doesn't find upgrades anymore.
	rm $pkgsup && touch $pkgsup
	newline
	gettext "Handled upgrades:"; colorize 32 " $up_ok"
	if [ "$up_fail" -gt 0 ]; then
		gettext "Failed upgrades:"; colorize 31 " $up_fail"
		gettext "Not upgraded:"; colorize 31 "$failed"
	fi
	# Packages that never made it past download (bad md5, missing on
	# mirror): reported here, not silently dropped. The upgrade list is
	# regenerated fresh on the next run, so they reappear then.
	if [ -n "$dl_fail" ]; then
		gettext "Failed downloads:"; colorize 31 "$dl_fail"
	fi
	newline
fi

exit 0
