#!/bin/sh
#
# tazhelper-reports - Read and triage TazHelper reports.
#
# Works on foyer, where tazhelper.cgi stores the reports, and on a dev
# machine with a local copy made by "pull". The state of a report is
# the directory it sits in: <release>/ is the inbox, <release>/done/
# holds the reports a developer has dealt with.
#
# Copyright (C) 2026 SliTaz GNU/Linux - BSD License
#

VERSION="0.1"

# Where tazhelper.cgi stores reports on foyer.
SERVER_DIR="/home/slitaz/vhosts/lab.slitaz.org/tazhelper-reports"

# On foyer work in place, elsewhere on a local copy. The remote (an ssh
# host) is where "done", "reopen" and "delete" really happen when we
# work on a copy; an empty TAZHELPER_REMOTE means a standalone copy.
if [ -d "$SERVER_DIR" ] && [ -z "$TAZHELPER_REPORTS" ]; then
	REPORTS="$SERVER_DIR"
	REMOTE=""
else
	REPORTS="${TAZHELPER_REPORTS:-$HOME/tazhelper-reports}"
	REMOTE="${TAZHELPER_REMOTE-foyer-vps}"
fi

# foyer blocks an address opening many ssh connections: TAZHELPER_SSH can
# reuse a shared one, e.g. "ssh -o ControlPath=/tmp/fy.sock" (ControlMaster).
SSH="${TAZHELPER_SSH:-ssh}"

# Reports and done/ directories are shared by the devs group.
umask 007

usage() {
	cat << EOT
Usage: tazhelper-reports <command> [args]

Read and triage the reports sent by TazHelper testers.

Commands:
  list [--done|--all]   Reports to deal with, oldest first
  show <id>             Print a report
  stats [--all]         Results per test, arch and flavor
  grep <regex> [--all]  Reports with a line matching regex
  done <id> [note]      Mark a report as dealt with (moved to done/)
  reopen <id>           Put a report back in the inbox
  delete <id>           Remove a report for good (spam, test)
  pull                  Copy the reports from $REMOTE to $REPORTS
                        (TAZHELPER_SSH: ssh command, to share a connection)

Options:
  --done                Only the reports already dealt with
  --all                 Inbox and done reports

Examples:
  tazhelper-reports list
  tazhelper-reports grep 'test.network=fail'
  tazhelper-reports done 20261003-081512-3fa9c2d1 fixed in wok r28600

About:
  Version $VERSION. Reports: $REPORTS
  Remote: ${REMOTE:-none, this is where reports live}
EOT
}

die() {
	echo "tazhelper-reports: $*" >&2
	exit 1
}

# Report files, oldest first: inbox, done (--done) or both (--all).
# The name starts with the UTC time of reception, so sorting the base
# names sorts them in time.
files() {
	case "$1" in
		--done) set -- "$REPORTS"/*/done/*.txt ;;
		--all) set -- "$REPORTS"/*/*.txt "$REPORTS"/*/done/*.txt ;;
		*) set -- "$REPORTS"/*/*.txt ;;
	esac
	for f in "$@"; do
		[ -f "$f" ] && echo "${f##*/} $f"
	done | sort | cut -d ' ' -f 2-
}

# Path of a report from its id (or a unique start of it).
find_report() {
	case "$1" in
		""|*[!a-z0-9-]*) die "bad report id: $1" ;;
	esac
	found="$(files --all | grep "/$1[^/]*\.txt$")"
	[ -n "$found" ] || die "no report: $1"
	[ "$(echo "$found" | wc -l)" = "1" ] || die "several reports match: $1"
	echo "$found"
}

# One line per report: id, release, arch, flavor, failed and partial
# tests, then the start of the general comment.
summary() {
	while read -r f; do
		rel="${f#$REPORTS/}"
		awk -F= -v id="$(basename "$f" .txt)" -v rel="${rel%%/*}" '
			$1 == "arch" { arch = $2 }
			$1 == "flavor" { flavor = $2 }
			$1 == "comment" { comment = substr($0, 9) }
			$1 ~ /^test\.[a-z0-9_-]+$/ {
				t = substr($1, 6)
				if ($2 == "fail") fail = fail (fail ? "," : "") t
				if ($2 == "partial") part = part (part ? "," : "") t
			}
			END {
				line = sprintf("%s  %s %s %s", id, rel, arch, flavor)
				if (fail) line = line "  fail:" fail
				if (part) line = line "  partial:" part
				if (comment) line = line "  \"" substr(comment, 1, 50) \
					(length(comment) > 50 ? "..." : "") "\""
				print line
			}' "$f"
	done
}

list() {
	out="$(files "$1" | summary)"
	if [ -z "$out" ]; then
		echo "No reports."
		return
	fi
	echo "$out"
	echo "$(echo "$out" | wc -l) report(s)"
}

# Status counts per test, then per arch and flavor.
stats() {
	set -- $(files "$1")
	[ "$#" -gt 0 ] || { echo "No reports."; return; }
	echo "$# report(s)"
	echo
	awk -F= '
		$1 ~ /^test\.[a-z0-9_-]+$/ { n[substr($1, 6) " " $2]++; t[substr($1, 6)] = 1 }
		END {
			printf "%-14s %6s %8s %6s\n", "Test", "ok", "partial", "fail"
			for (i in t)
				printf "%-14s %6d %8d %6d\n", i, n[i " ok"], n[i " partial"], n[i " fail"]
		}' "$@" | { read -r head; echo "$head"; sort; }
	echo
	awk -F= '$1 == "arch" || $1 == "flavor" { print $1 " " $2 }' "$@" |
		sort | uniq -c | awk '{ printf "%-14s %6d\n", $2 "=" $3, $1 }'
}

grep_reports() {
	[ -n "$1" ] || die "grep needs a regex"
	files "$2" | while read -r f; do
		grep -qE -- "$1" "$f" && echo "$f"
	done | summary
}

# Rewrite a report into done/ with who and when: one file, so done
# reports stay greppable like the others.
mark_done() {
	f="$(find_report "$1")" || exit 1
	shift
	case "$f" in */done/*) die "already done: ${f##*/}" ;; esac
	dir="${f%/*}"
	mkdir -p "$dir/done"
	{
		cat "$f"
		echo "triage.date=$(date -u +%Y-%m-%d)"
		echo "triage.by=$(id -un)"
		if [ -n "$*" ]; then
			echo "triage.note=$(echo "$*" | tr -d '\000-\037')"
		fi
	} > "$dir/done/.${f##*/}" &&
		mv "$dir/done/.${f##*/}" "$dir/done/${f##*/}" &&
		rm -f "$f" &&
		echo "Done: ${f##*/}"
}

reopen() {
	f="$(find_report "$1")" || exit 1
	case "$f" in */done/*) ;; *) die "not done: ${f##*/}" ;; esac
	inbox="${f%/done/*}"
	grep -v '^triage\.' "$f" > "$inbox/.${f##*/}" &&
		mv "$inbox/.${f##*/}" "$inbox/${f##*/}" &&
		rm -f "$f" &&
		echo "Reopened: ${f##*/}"
}

delete() {
	f="$(find_report "$1")" || exit 1
	rm -f "$f" && echo "Deleted: ${f##*/}"
}

pull() {
	[ -n "$REMOTE" ] || die "no remote: reports already live here"
	mkdir -p "$REPORTS"
	rsync -a --delete --exclude .tmp/ -e "$SSH" \
		"$REMOTE:$SERVER_DIR/" "$REPORTS/" || return 1
	# rsync copies the server modes (devs group): the copy is private.
	chmod 700 "$REPORTS"
	echo "$(files | wc -l) report(s) to deal with in $REPORTS"
}

# Changes on a local copy happen on the remote first, then we pull.
# ssh joins its arguments into one remote command line: quote them.
remote() {
	cmd="tazhelper-reports"
	for arg in "$@"; do
		cmd="$cmd '$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")'"
	done
	$SSH "$REMOTE" "$cmd" && pull > /dev/null
}

case "$1" in
	list) list "$2" ;;
	show) f="$(find_report "$2")" && cat "$f" ;;
	stats) stats "$2" ;;
	grep) grep_reports "$2" "$3" ;;
	done) shift; [ -n "$REMOTE" ] && { remote done "$@"; exit; }
		mark_done "$@" ;;
	reopen) [ -n "$REMOTE" ] && { remote reopen "$2"; exit; }
		reopen "$2" ;;
	delete) [ -n "$REMOTE" ] && { remote delete "$2"; exit; }
		delete "$2" ;;
	pull) pull ;;
	*) usage ;;
esac
