#!/bin/sh
#
# cook-doctor - SliTaz cook: diagnose the build environment and packages.
# Runs a battery of read-only health checks and, for each problem found,
# prints a suggested fix command (it never runs anything itself in this
# release -- the --fix automation is planned, see the fix() seam below).
#
# Usage:
#   cook-doctor                  # full diagnosis of the build environment
#   cook-doctor env              # same as no argument (environment only)
#   cook-doctor <pkg>            # diagnose one wok package / receipt
#
# Each check prints one of:
#   ok    green  v   the check passed
#   note  yellow !   advisory, build still works
#   bad   red    x   real problem, with a suggested fix below it
#
# Copyright (C) SliTaz GNU/Linux - GNU GPL v3
#

. /usr/lib/slitaz/libcook.sh

# cook exports output=raw so captured build logs stay plain text for the
# cooker's HTML highlighter. Reached through the `cook doctor` shim we
# inherit that and colorize() drops every color. Restore ANSI only on a
# real terminal; keep raw for piped/captured output (logs, CI, the
# cooker), and leave html in place under a CGI referer.
if [ -z "$HTTP_REFERER" ]; then
	[ -t 1 ] && unset output || output=raw
fi


#
# Diagnostic framework
#

dr_ok=0; dr_note=0; dr_bad=0
# In quiet mode (the `all` loop) the primitives only count -- nothing is
# printed, and the loop emits one summary line per non-healthy package.
dr_quiet=''

# Section banner.
section() { [ -n "$dr_quiet" ] && return; newline; boldify "$1"; }

# Check verdicts. Glyphs stay ASCII-narrow so columns line up in any term.
ok()   { dr_ok=$((  dr_ok   + 1)); [ -n "$dr_quiet" ] || echo " $(colorize 32 'v') $1"; }
note() { dr_note=$(( dr_note + 1)); [ -n "$dr_quiet" ] || echo " $(colorize 33 '!') $1"; }
bad()  { dr_bad=$((  dr_bad  + 1)); [ -n "$dr_quiet" ] || echo " $(colorize 31 'x') $1"; }

# Suggested remediation, printed under the note/bad it belongs to.
#
# v1 only prints the command -- cook-doctor never touches the system on
# its own. This single function is the seam for a future opt-in --fix:
# when that lands, branch here on a $DOCTOR_FIX flag to eval "$1".
fix() { [ -n "$dr_quiet" ] || echo "     $(colorize 36 "$(_ 'fix:')") $1"; }


usage() {
	cat <<EOT

$(boldify "$(_ 'Usage:')") cook-doctor [command|package]

$(boldify "$(_ 'Commands:')")
  usage|help|--help|-h $(_ 'Display this short usage.')
  (no arg)|env         $(_ 'Diagnose the build environment.')
  <package>            $(_ 'Diagnose one wok package / receipt.')
  all [<prefix>]       $(_ 'Scan the wok, one summary line per problem.')

$(boldify "$(_ 'Examples:')")
  cook-doctor                  $(_ 'health-check the build environment')
  cook-doctor busybox          $(_ 'diagnose the busybox receipt and package')
  cook-doctor all              $(_ 'scan the whole wok for problems')
  cook-doctor all lib          $(_ 'scan only $WOK/lib* receipts')

$(boldify "$(_ 'About fixes:')")
  cook-doctor only diagnoses. For every problem it prints a suggested
  fix command but never runs it -- copy/paste what you trust. Opt-in
  automatic repair is planned for a later release.

$(boldify "$(_ 'Exit status:')")
  0 if nothing is broken, 1 if any problem (x) was found -- usable as a
  gate in cook all / the cooker / CI.

EOT
	exit 0
}


#
# Environment diagnosis
#

diagnose_env() {
	title 'Cook environment diagnosis (%s)' "$ARCH"

	# --- Config & directory layout -------------------------------------
	section "$(_ 'Configuration')"
	if [ -f /etc/slitaz/cook.conf ]; then
		ok "$(_ 'cook.conf present')"
	else
		bad "$(_ 'missing /etc/slitaz/cook.conf')"
		fix 'cook setup'
	fi

	for dir in SLITAZ WOK PKGS SRC CACHE LOGS; do
		eval path=\$$dir
		if [ -d "$path" ]; then
			ok "$(_ '%s -> %s' "$dir" "$path")"
		else
			bad "$(_ 'missing %s directory: %s' "$dir" "$path")"
			fix "cook setup"
		fi
	done

	# --- Cook DB files -------------------------------------------------
	section "$(_ 'Cache DB files')"
	for db in activity command broken blocked; do
		eval f=\$$db
		[ -z "$f" ] && f="$cache/$db"
		if [ -f "$f" ]; then
			ok "$(basename "$f")"
		else
			bad "$(_ 'missing DB file: %s' "$f")"
			fix "touch $f"
		fi
	done

	# --- Disk space ----------------------------------------------------
	section "$(_ 'Disk space')"
	if [ -d "$SLITAZ" ]; then
		free_mb=$(df -P "$SLITAZ" | awk 'NR==2{print int($4/1024)}')
		if [ "$free_mb" -lt 512 ]; then
			bad "$(_ 'only %s MB free on %s' "$free_mb" "$SLITAZ")"
			fix 'cook clean-wok; cook clean-src'
		elif [ "$free_mb" -lt 2048 ]; then
			note "$(_ '%s MB free on %s (low)' "$free_mb" "$SLITAZ")"
			fix 'cook clean-src'
		else
			ok "$(_ '%s MB free on %s' "$free_mb" "$SLITAZ")"
		fi
	fi

	# --- Orphan chroot mounts ------------------------------------------
	section "$(_ 'Chroot mounts')"
	# cook builds in /dev/shm/aufsmnt$$ (or /mnt/aufsmnt$$); a mount left
	# behind after a Ctrl-C or a crash shows up as an aufsmnt*root branch.
	orphans=$(awk '$2 ~ /aufsmnt[0-9]+root/ {print $2}' /proc/mounts \
		| sort -u)
	if [ -n "$orphans" ]; then
		for mnt in $orphans; do
			bad "$(_ 'orphan chroot mount: %s' "$mnt")"
			fix "umount -l $mnt"
		done
	else
		ok "$(_ 'no orphan chroot mounts')"
	fi

	# --- Toolchain & setup packages ------------------------------------
	section "$(_ 'Toolchain')"
	if [ -d "$INSTALLED/slitaz-toolchain" ]; then
		ok "$(_ 'slitaz-toolchain installed')"
	else
		bad "$(_ 'slitaz-toolchain not installed')"
		fix 'cook setup --forced'
	fi

	missing=''
	for pkg in $SETUP_PKGS; do
		[ -d "$INSTALLED/$pkg" ] || missing="$missing $pkg"
	done
	if [ -n "$missing" ]; then
		note "$(_ 'missing setup packages:%s' "$missing")"
		fix 'cook setup'
	else
		ok "$(_ 'all SETUP_PKGS installed')"
	fi

	# cook itself aborts with "Broken setup" on this mismatch.
	if [ -n "$SETUP_MD5" ]; then
		cur=$(ls "$INSTALLED" | md5sum | cut -c1-32)
		if [ "$SETUP_MD5" = "$cur" ]; then
			ok "$(_ 'SETUP_MD5 matches installed set')"
		else
			bad "$(_ 'SETUP_MD5 mismatch (cook will refuse to build)')"
			fix 'sed -i "s|SETUP_MD5=.*|SETUP_MD5=\"\"|" /etc/slitaz/cook.conf'
		fi
	fi

	# --- Arch coherence ------------------------------------------------
	# arm* is always cross-built (on an x86 host) so it needs a CROSS_TREE.
	# i486 and x86_64 are native primary arches: a build is native unless a
	# CROSS_TREE is explicitly configured, in which case we check it exists.
	section "$(_ 'Architecture')"
	case "$ARCH" in
		arm*)
			if [ -n "$CROSS_TREE" ] && [ -d "$CROSS_TREE" ]; then
				ok "$(_ 'cross tree for %s: %s' "$ARCH" "$CROSS_TREE")"
			else
				bad "$(_ 'ARCH=%s but CROSS_TREE missing: %s' "$ARCH" "$CROSS_TREE")"
				fix "cook $ARCH-setup"
			fi
			;;
		*)
			if [ -n "$CROSS_TREE" ]; then
				if [ -d "$CROSS_TREE" ]; then
					ok "$(_ 'cross tree for %s: %s' "$ARCH" "$CROSS_TREE")"
				else
					bad "$(_ 'CROSS_TREE set but missing: %s' "$CROSS_TREE")"
					fix "cook $ARCH-setup"
				fi
			else
				ok "$(_ 'native build, ARCH=%s' "$ARCH")"
			fi
			;;
	esac

	# --- Permissions ---------------------------------------------------
	section "$(_ 'Permissions')"
	if grep -q '^slitaz' /etc/group; then
		ok "$(_ 'group "slitaz" present')"
	else
		note "$(_ 'group "slitaz" missing')"
		fix 'cook setup'
	fi

	# --- Network-dependent toggles -------------------------------------
	# Only worth checking when the user opted in; a dev chroot leaves
	# these "no" on purpose and we must not nag about the network then.
	if [ "$MAKE_BUNDLE" = 'yes' ] || [ "$REPOLOGY_CHECK" = 'yes' ]; then
		section "$(_ 'Network toggles')"
		[ "$MAKE_BUNDLE" = 'yes' ] && net_check mirror1.slitaz.org \
			"$(_ 'MAKE_BUNDLE')"
		[ "$REPOLOGY_CHECK" = 'yes' ] && net_check repology.org \
			"$(_ 'REPOLOGY_CHECK')"
	fi

	verdict
}

# Warn (never fail) if a toggle wants a host we cannot reach: the build
# still succeeds, the feature just degrades.
net_check() {
	if busybox wget -T 8 --spider "https://$1" 2>/dev/null; then
		ok "$(_ '%s enabled, %s reachable' "$2" "$1")"
	else
		note "$(_ '%s enabled but %s unreachable' "$2" "$1")"
		fix "$(_ 'set %s=\"no\" in cook.conf or fix connectivity' "$2")"
	fi
}


#
# Package diagnosis
#

diagnose_pkg() {
	title 'Package diagnosis: %s' "$1"
	check_pkg "$1"
	verdict
}

# Per-package checks. Uses ok/note/bad only -- no title, no verdict --
# so the `all` loop can run it in quiet mode and read the counter deltas.
# Receipt vars are unset up front so values can't leak across iterations.
check_pkg() {
	pkg="$1"
	unset PACKAGE VERSION CATEGORY SHORT_DESC MAINTAINER WEB_SITE \
		WANTED BUILD_DEPENDS DEPENDS TARBALL EXTRAVERSION HOST_ARCH

	# --- Receipt sanity (mirrors modules/precheck) ---------------------
	section "$(_ 'Receipt')"
	receipt="$WOK/$pkg/receipt"
	if [ ! -f "$receipt" ]; then
		bad "$(_ 'no receipt: %s' "$receipt")"
		fix "cook new $pkg"
		return
	fi
	ok "$(_ 'receipt present')"

	# Source in a subshell so a broken receipt can't poison our env.
	( . "$receipt" ) 2>/dev/null || { bad "$(_ 'receipt is not sourceable')"; }
	. "$receipt"

	for var in PACKAGE VERSION CATEGORY SHORT_DESC MAINTAINER WEB_SITE; do
		eval value=\$$var
		if [ -n "$value" ]; then
			ok "$var=\"$value\""
		else
			bad "$(_ 'empty variable: %s' "$var")"
			fix "$(_ 'edit %s' "$receipt")"
		fi
	done

	valid="$(echo $PKGS_CATEGORIES nopack)"
	if echo " $valid " | grep -q " $CATEGORY "; then
		ok "$(_ 'category valid')"
	else
		bad "$(_ 'unknown category: %s' "$CATEGORY")"
		fix "$(_ 'use one of: %s' "$valid")"
	fi

	if [ "$PACKAGE" = "$pkg" ]; then
		ok "$(_ 'PACKAGE matches directory name')"
	else
		bad "$(_ 'PACKAGE="%s" but directory is "%s"' "$PACKAGE" "$pkg")"
		fix "$(_ 'rename the wok directory or fix PACKAGE')"
	fi

	# --- Dependency targets exist in the wok ---------------------------
	section "$(_ 'Dependencies')"
	dep_miss=''
	for d in $WANTED $BUILD_DEPENDS; do
		[ -d "$WOK/$d" ] || dep_miss="$dep_miss $d"
	done
	if [ -n "$dep_miss" ]; then
		bad "$(_ 'build deps not in wok:%s' "$dep_miss")"
		fix "cook setup --wok"
	else
		ok "$(_ 'all WANTED / BUILD_DEPENDS present in wok')"
	fi

	# --- Broken state --------------------------------------------------
	section "$(_ 'Build state')"
	if grep -qs "^$pkg$" "$broken"; then
		bad "$(_ 'listed as broken')"
		if [ -f "$LOGS/$pkg.log" ]; then
			echo "     $(colorize 90 "$(tail -n 3 "$LOGS/$pkg.log")")"
		fi
		fix "cook $pkg"
	else
		ok "$(_ 'not in broken list')"
	fi

	# Stale taz/: receipt edited after the package was last packed.
	if [ -d "$WOK/$pkg/taz" ]; then
		if [ "$receipt" -nt "$WOK/$pkg/taz" ]; then
			note "$(_ 'receipt newer than taz/ (needs recook)')"
			fix "cook $pkg"
		else
			ok "$(_ 'taz/ up to date with receipt')"
		fi
	fi

	# --- Resulting tazpkg ----------------------------------------------
	section "$(_ 'Package file')"
	case "$ARCH" in
		i486) suffix='' ;;          # i486 keeps no arch suffix (legacy)
		*)    suffix="-$ARCH" ;;
	esac
	tazpkg="$PKGS/$PACKAGE-$VERSION$EXTRAVERSION$suffix.tazpkg"
	any="$PKGS/$PACKAGE-$VERSION$EXTRAVERSION-any.tazpkg"
	if [ -f "$tazpkg" ] || [ -f "$any" ]; then
		ok "$(_ 'cooked package present')"
	else
		note "$(_ 'no cooked package for %s' "$VERSION$EXTRAVERSION")"
		fix "cook $pkg"
	fi

	# --- Source tarball ------------------------------------------------
	if [ -n "$TARBALL" ]; then
		if [ -f "$SRC/$TARBALL" ]; then
			ok "$(_ 'source tarball present')"
		else
			note "$(_ 'source tarball not fetched: %s' "$TARBALL")"
			fix "cook $pkg --getsrc"
		fi
	fi
}


#
# Whole-wok scan
#

# One summary line per non-healthy package: "<name>  N problems, M warnings".
report_line() {
	local name="$1" nb="$2" nn="$3" parts=''
	[ "$nb" -gt 0 ] && parts="$(colorize 31 "$(_p '%s problem' '%s problems' "$nb" "$nb")")"
	if [ "$nn" -gt 0 ]; then
		[ -n "$parts" ] && parts="$parts, "
		parts="$parts$(colorize 33 "$(_p '%s warning' '%s warnings' "$nn" "$nn")")"
	fi
	printf ' %-24s %s\n' "$name" "$parts"
}

diagnose_all() {
	prefix="$1"
	title 'Scanning the wok for problems (%s)' "$ARCH"
	newline
	scanned=0; healthy=0; warn_pkgs=0; prob_pkgs=0
	dr_quiet='yes'

	for receipt in $WOK/$prefix*/receipt; do
		[ -f "$receipt" ] || continue
		p=$(basename "$(dirname "$receipt")")

		# ARCH scope: arm* ports only build receipts whose HOST_ARCH
		# matches; i486 and x86_64 are primary arches and scan the whole
		# wok (same rule as cook-wok uncook).
		case "$ARCH" in
			arm*)
				ha="$(. "$receipt"; echo "$HOST_ARCH")"
				echo "$ha" | egrep -q "$ARCH|any" || continue ;;
		esac

		scanned=$((scanned + 1))
		b0="$dr_bad"; n0="$dr_note"
		check_pkg "$p"
		nb=$((dr_bad - b0)); nn=$((dr_note - n0))

		if [ "$nb" -gt 0 ]; then
			prob_pkgs=$((prob_pkgs + 1)); report_line "$p" "$nb" "$nn"
		elif [ "$nn" -gt 0 ]; then
			warn_pkgs=$((warn_pkgs + 1)); report_line "$p" "$nb" "$nn"
		else
			healthy=$((healthy + 1))
		fi
	done

	newline
	sum="$(_ '%s scanned' "$scanned"), $(_ '%s healthy' "$(colorize 32 "$healthy")")"
	[ "$warn_pkgs" -gt 0 ] && \
		sum="$sum, $(colorize 33 "$(_p '%s warning' '%s warnings' "$warn_pkgs" "$warn_pkgs")")"
	[ "$prob_pkgs" -gt 0 ] && \
		sum="$sum, $(colorize 31 "$(_p '%s problem' '%s problems' "$prob_pkgs" "$prob_pkgs")")"
	footer "$sum"
	[ "$prob_pkgs" -gt 0 ] && exit 1 || exit 0
}


#
# Final verdict (env / single package)
#

verdict() {
	newline
	if [ "$dr_bad" -gt 0 ]; then
		msg="$(colorize 31 "$(_p '%s problem' '%s problems' "$dr_bad" "$dr_bad")")"
	elif [ "$dr_note" -gt 0 ]; then
		msg="$(colorize 33 "$(_p '%s warning' '%s warnings' "$dr_note" "$dr_note")")"
	else
		msg="$(colorize 32 "$(_ 'healthy')")"
	fi
	footer "$(_ '%s checks, %s' "$((dr_ok + dr_note + dr_bad))" "$msg")"
	[ "$dr_bad" -gt 0 ] && exit 1 || exit 0
}


#
# Dispatch
#

case "$1" in
	usage|help|--help|-h) usage ;;
	''|env)               diagnose_env ;;
	all)                  diagnose_all "$2" ;;
	*)
		if [ -d "$WOK/$1" ] || [ -f "$WOK/$1/receipt" ]; then
			diagnose_pkg "$1"
		else
			_ 'No such wok package: %s' "$1" >&2
			usage
		fi
		;;
esac
