#!/bin/sh
#
# Spk-search - Find SliTaz packages, files, and provides. 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

fileslist="${root}${PKGS_DB}/files-list.lzma"

#
# Usage
#

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

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

$(boldify $(gettext "Search targets (default: packages by name + desc):"))
  --name        $(gettext "Match package name only")
  --desc        $(gettext "Match short description only")
  --tag         $(gettext "Match tags field")
  --cat=CAT     $(gettext "Restrict to a category")
  --file        $(gettext "Find which package(s) provide a file path")
  --provide     $(gettext "Lookup an alias / PROVIDE name (packages.equiv)")
  --rdeps       $(gettext "Reverse deps: who depends on the given package")

$(boldify $(gettext "Source filter:"))
  --src=SRC     $(gettext "Restrict to: installed | mirror | extra (default: all)")

$(boldify $(gettext "Output:"))
  --short       $(gettext "One line per result (no description)")
  --raw         $(gettext "Plain TSV, scriptable, stable format")
  --trame       $(gettext "JSON object on stdout, errors as JSON on stderr")

EOT
	exit 0
}

case "$1" in
	-h|--help|*usage) usage ;;
esac

# Pattern = first non-option argument, so "--file PATTERN" works just like
# "PATTERN --file" (libtaz already sets the flags regardless of position).
pattern=$(first_operand "$@")
[ "$pattern" ] || usage

# Validate the pattern as a regex ONCE. Every search path greps it against
# each catalogue line, so an invalid regex (e.g. "foo[") otherwise spat one
# "grep: Unmatched [" per package to stderr and then reported 0 found. If it
# isn't a valid regex, search it as a literal string (-F) instead of failing.
# grep exits >=2 on a malformed pattern (0/1 = matched / not-matched).
gfix=
if [ "$(echo x | grep -e "$pattern" >/dev/null 2>&1; echo $?)" -ge 2 ]; then
	gfix=-F
fi

# Bail out helpers — emit JSON on stderr if --trame, plain text otherwise.
die() {
	local code="$1"; shift
	local msg="$*"
	if [ "$trame" ]; then
		printf '{"error":"%s","code":%s,"command":"search"}\n' \
			"$(json_escape "$msg")" "$code" >&2
	else
		echo "spk-search: $msg" >&2
	fi
	exit "$code"
}

# JSON-escape a string (busybox sed): backslash, quote, then control chars.
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'
}

# Resolve the --src filter into a list of packages.info to scan.
# Value "installed" is special — handled by the caller via walk_installed.
pkgsinfo_sources() {
	local info
	case "$src" in
	installed) echo "installed" ;;
	extra)
		for info in $extradb/*/packages.info; do
			[ -f "$info" ] && echo "$info"
		done
		;;
	mirror)
		[ -f "$pkgsinfo" ] && echo "$pkgsinfo"
		;;
	*)
		# Default: extras first (priority), then official, like mirrored_pkg().
		for info in $extradb/*/packages.info; do
			[ -f "$info" ] && echo "$info"
		done
		[ -f "$pkgsinfo" ] && echo "$pkgsinfo"
		;;
	esac
}

# Match a pattern against the relevant TSV field(s).
# Sets `hit=1` (return 0) if the line matches, else returns 1.
match_pkg_line() {
	local pkg="$1" ver="$2" cat="$3" desc="$4" tags="$5"
	[ "$cat_filter" ] && [ "$cat" != "$cat_filter" ] && return 1
	if [ "$name" ]; then
		echo "$pkg" | grep -qi $gfix -- "$pattern"
	elif [ "$desc_only" ]; then
		echo "$desc" | grep -qi $gfix -- "$pattern"
	elif [ "$tag" ]; then
		echo "$tags" | grep -qi $gfix -- "$pattern"
	else
		# Smart: name + desc.
		echo "$pkg $desc" | grep -qi $gfix -- "$pattern"
	fi
}

# Render one package hit. $1=pkg $2=ver $3=cat $4=desc $5=source(mirror|extra|installed)
render_pkg() {
	local pkg="$1" ver="$2" cat="$3" desc="$4" src="$5"
	local inst=false
	is_package_installed "$pkg" && inst=true
	case "$output" in
	raw)
		printf 'package\t%s\t%s\t%s\t%s\t%s\t%s\n' \
			"$pkg" "$ver" "$cat" "$desc" "$src" "$inst"
		;;
	trame)
		[ "$first" ] || printf ','
		first=
		printf '{"package":"%s","version":"%s","category":"%s","short_desc":"%s","source":"%s","installed":%s}' \
			"$(json_escape "$pkg")" "$(json_escape "$ver")" \
			"$(json_escape "$cat")" "$(json_escape "$desc")" \
			"$src" "$inst"
		;;
	*)
		local tag=
		[ "$inst" = true ] && tag=" $(colorize 33 '[installed]')"
		if [ "$short" ]; then
			printf '%s %s%s\n' \
				"$(colorize 34 "$pkg")" "$ver" "$tag"
		else
			printf '%s %s%s\n' \
				"$(colorize 34 "$pkg")" "$ver" "$tag"
			[ "$desc" ] && echo "  $desc"
		fi
		;;
	esac
	count=$((count + 1))
}

# Render one file hit. $1=pkg $2=path $3=source
render_file() {
	local pkg="$1" path="$2" src="$3"
	local inst=false
	is_package_installed "$pkg" && inst=true
	case "$output" in
	raw)
		printf 'file\t%s\t%s\t%s\t%s\n' "$pkg" "$path" "$src" "$inst"
		;;
	trame)
		[ "$first" ] || printf ','
		first=
		printf '{"package":"%s","path":"%s","source":"%s","installed":%s}' \
			"$(json_escape "$pkg")" "$(json_escape "$path")" \
			"$src" "$inst"
		;;
	*)
		local tag=
		[ "$inst" = true ] && tag=" $(colorize 33 '[installed]')"
		printf '%s%s %s\n' "$(colorize 34 "$pkg")" "$tag" "$path"
		;;
	esac
	count=$((count + 1))
}

#
# Search modes
#

search_packages() {
	local sources line pkg ver cat desc rest info from tags
	sources=$(pkgsinfo_sources)
	[ "$sources" ] || die 2 "no package database found"

	for info in $sources; do
		if [ "$info" = "installed" ]; then
			# Walk receipts; build a TSV-like line on the fly.
			for d in $installed/*; do
				[ -f "$d/receipt" ] || continue
				unset_receipt
				. $d/receipt
				pkg="$PACKAGE"
				ver="${VERSION}${EXTRAVERSION}"
				cat="$CATEGORY"
				desc="$SHORT_DESC"
				tags="$TAGS"
				match_pkg_line "$pkg" "$ver" "$cat" "$desc" "$tags" || continue
				render_pkg "$pkg" "$ver" "$cat" "$desc" installed
			done
		else
			# Identify mirror flavor for the `source` field.
			case "$info" in
			$extradb/*) from=$(basename $(dirname "$info")) ;;
			*) from=mirror ;;
			esac
			while IFS="	" read pkg ver cat desc web tags rest; do
				match_pkg_line "$pkg" "$ver" "$cat" "$desc" "$tags" || continue
				render_pkg "$pkg" "$ver" "$cat" "$desc" "$from"
			done < "$info"
		fi
	done
}

search_files() {
	local pkg path tag from buf="$tmpdir/files.tsv"
	mkdir -p "$tmpdir"
	# Materialize "pkg<TAB>path<TAB>source" lines first, then render in the
	# parent shell so count/first survive (no pipe → no subshell).
	if [ "$src" != installed ]; then
		if spk_sql_ready; then
			# SQL fast path: matching catalog files straight from the index,
			# no lzma decompression. 'official' is reported as 'mirror' to
			# match the flat-file source label. Substring (LIKE) match.
			# Escape the LIKE wildcards % and _ (and the escape char \) so the
			# pattern is a LITERAL substring — matching the awk/index() path
			# below; without this, --file 'lib_c' had _ match any char in SQL
			# but stay literal in the flat-file fallback. '' escapes the quote.
			local esc=$(printf '%s' "$pattern" \
				| sed -e 's/\\/\\\\/g' -e 's/%/\\%/g' -e 's/_/\\_/g' -e "s/'/''/g")
			local where="path LIKE '%${esc}%' ESCAPE '\\'"
			[ "$src" = mirror ] && where="$where AND repo='official'"
			[ "$src" = extra ]  && where="$where AND repo!='official'"
			sql ".mode tabs" \
				"SELECT name, path,
				        CASE WHEN repo='official' THEN 'mirror' ELSE repo END
				 FROM file WHERE $where;" >> "$buf"
		else
			# Match the PATH only, case-insensitively, as a literal substring
			# — same semantics as the SQL fast path (path LIKE '%p%'). The old
			# `grep "$pattern"` scanned the whole "pkg: /path" line, so it also
			# matched the package NAME (e.g. --file pam hit "pam: …") and was
			# case-sensitive, giving results the SQL path never returned.
			if [ "$src" != extra ] && [ -f "$fileslist" ]; then
				busybox unlzma -c "$fileslist" 2>/dev/null \
				| awk -v p="$pattern" 'BEGIN{lp=tolower(p)}
					{ i=index($0,": "); if(!i) next
					  path=substr($0,i+2)
					  if(index(tolower(path),lp))
						print substr($0,1,i-1)"\t"path"\tmirror" }' >> "$buf"
			fi
			if [ "$src" != mirror ]; then
				for x in $extradb/*/files-list.lzma; do
					[ -f "$x" ] || continue
					tag=$(basename $(dirname "$x"))
					busybox unlzma -c "$x" 2>/dev/null \
					| awk -v p="$pattern" -v tag="$tag" 'BEGIN{lp=tolower(p)}
						{ i=index($0,": "); if(!i) next
						  path=substr($0,i+2)
						  if(index(tolower(path),lp))
							print substr($0,1,i-1)"\t"path"\t"tag }' >> "$buf"
				done
			fi
		fi
	fi
	if [ "$src" = installed ] || [ -z "$src" ]; then
		for d in $installed/*; do
			[ -f "$d/files.list" ] || continue
			pkg=$(basename "$d")
			# installed files.list is already "/path" per line (no name
			# prefix); match case-insensitively, literal substring, to stay
			# consistent with the mirror/SQL path-only semantics above.
			awk -v p="$pattern" -v pkg="$pkg" 'BEGIN{lp=tolower(p)}
				index(tolower($0),lp){print pkg"\t"$0"\tinstalled"}' \
				"$d/files.list" >> "$buf"
		done
	fi
	[ -f "$buf" ] || return 0
	while IFS="	" read pkg path from; do
		render_file "$pkg" "$path" "$from"
	done < "$buf"
}

search_provides() {
	[ -f "$pkgsequiv" ] || die 2 "packages.equiv missing"
	local alias pkgs pkg buf="$tmpdir/provides.tsv"
	mkdir -p "$tmpdir"
	grep $gfix -- "$pattern" "$pkgsequiv" > "$buf" 2>/dev/null
	[ -s "$buf" ] || return 0
	while IFS='=' read alias pkgs; do
		[ "$alias" ] || continue
		for pkg in $pkgs; do
			case "$output" in
			raw)
				printf 'provide\t%s\t%s\n' "$alias" "$pkg"
				;;
			trame)
				[ "$first" ] || printf ','
				first=
				printf '{"alias":"%s","package":"%s"}' \
					"$(json_escape "$alias")" "$(json_escape "$pkg")"
				;;
			*)
				printf '%s -> %s\n' \
					"$(colorize 36 "$alias")" "$(colorize 34 "$pkg")"
				;;
			esac
			count=$((count + 1))
		done
	done < "$buf"
}

# Who depends on $pattern ? Walks installed receipts and mirror
# packages.info (col 8 = DEPENDS). Matches whole-word inside the
# space-delimited DEPENDS string so "ncurses" never matches
# "ncurses-common".
search_rdeps() {
	local d pkg deps from line ver
	# Installed side: walk receipts.
	if [ "$src" != mirror ] && [ "$src" != extra ]; then
		for d in $installed/*; do
			[ -f "$d/receipt" ] || continue
			unset_receipt
			. $d/receipt
			pkg="$PACKAGE"
			ver="${VERSION}${EXTRAVERSION}"
			deps=" $DEPENDS "
			case "$deps" in
				*\ "$pattern"\ *)
					render_pkg "$pkg" "$ver" "$CATEGORY" \
						"$SHORT_DESC" installed
					;;
			esac
		done
	fi
	# Mirror side: scan packages.info TSV (col 8 = DEPENDS). Materialize the
	# awk matches into a buffer first, then render in the parent shell — the
	# old `awk | while read render_pkg` ran render_pkg in the pipe's subshell,
	# so every count/first update was lost: results printed but "rdeps found"
	# stayed 0 (exit 1), and --trame emitted a comma-less, invalid array.
	# Same fix search_files() already uses.
	if [ "$src" != installed ]; then
		local sources info buf="$tmpdir/rdeps.tsv"
		mkdir -p "$tmpdir"; : > "$buf"
		sources=$(pkgsinfo_sources)
		for info in $sources; do
			[ "$info" = "installed" ] && continue
			case "$info" in
			$extradb/*) from=$(basename $(dirname "$info")) ;;
			*) from=mirror ;;
			esac
			# Use awk to whole-word match field 8 (DEPENDS).
			awk -F'	' -v p="$pattern" -v from="$from" '
				{
					n = split($8, dep, " ");
					for (i = 1; i <= n; i++) if (dep[i] == p) {
						printf "%s\t%s\t%s\t%s\t%s\n",
							$1, $2, $3, $4, from;
						next;
					}
				}' "$info" >> "$buf"
		done
		while IFS="	" read pkg ver cat desc src_; do
			# Skip duplicates already shown via installed pass.
			[ "$src" = "" ] && is_package_installed "$pkg" && continue
			render_pkg "$pkg" "$ver" "$cat" "$desc" "$src_"
		done < "$buf"
	fi
}

#
# Main
#

# Dispatch output mode (mutually exclusive — last wins, raw/trame imply !color).
output=human
[ "$raw" ] && output=raw
[ "$trame" ] && output=trame

# Honor TTY for colorization in human mode.
if [ "$output" = human ] && [ ! -t 1 ]; then
	colorize() { shift; echo "$@"; }
fi

# `desc` is also a libtaz option name, but we set it from --desc here too.
# Use a distinct internal var to avoid shadowing.
[ "$desc" ] && desc_only=yes

# `--cat=X` is parsed by libtaz as `cat=X` — alias to cat_filter to avoid
# clashing with the shell `cat` builtin-ish reflex.
cat_filter="$cat"

kind=package
[ "$file" ] && kind=file
[ "$provide" ] && kind=provide
[ "$rdeps" ] && kind=rdeps

# In trame mode, emit a single JSON object: header → results array → footer.
if [ "$output" = trame ]; then
	printf '{"query":"%s","kind":"%s","results":[' \
		"$(json_escape "$pattern")" "$kind"
	first=yes
fi

if [ "$output" = human ]; then
	newline
	boldify "$(gettext 'Spk search:') $pattern"
	separator
fi

count=0
case "$kind" in
	file)    search_files ;;
	provide) search_provides ;;
	rdeps)   search_rdeps ;;
	*)       search_packages ;;
esac

if [ "$output" = trame ]; then
	printf '],"count":%s}\n' "$count"
elif [ "$output" = human ]; then
	separator
	color=32
	[ "$count" -eq 0 ] && color=31
	# `${kind}s` would yield "rdepss" — special-case the plural.
	case "$kind" in
		rdeps) label=$(gettext "rdeps found:") ;;
		*)     label=$(gettext "${kind}s found:") ;;
	esac
	boldify "$label $(colorize $color $count)"
	newline
fi

[ -d "$tmpdir" ] && rm -rf "$tmpdir"
[ "$count" -gt 0 ] || exit 1
exit 0
