#!/usr/bin/env bash
# health — say whether this repository is fit to have work dispatched into it.
#
# ENVIRONMENT (this script receives NO factory variables):
#   (none) — it starts in the repository's registered directory.
#
# WHAT IT CHECKS, in order, cheapest first:
#   1. pwd is a Git repository with an origin
#   2. the local working tree's clean or uncommitted state
#   3. origin refreshes with `timeout -k 10 60 git fetch --quiet origin`
#      when timeout is available, or directly with Git otherwise; one timeout
#      is retried so an unreachable remote is unfit, not a hung doctor
#   4. the refreshed origin/main resolves, then committed HEAD's deployment
#      state
#   5. the active contract folders exist: project, scripts, tickets, and records
#   6. the contract zone's shape: numbered tickets and records, plus dated or
#      legacy-numbered issues when a transitional issues folder exists; no legacy
#      issue number may be used by a ticket or record (sdlc ADR 0018).
#      planning/ is deliberately unchecked — nothing reads it
#   7. each dispatchable origin ticket's opens declaration through the canonical
#      build path policy
#   8. the registered directory's filesystem has at least 10 GiB free
#   9. an optional executable sdlc/scripts/health project extension
#
# STDOUT:  local working-tree and deployment state, plus canonical and project
#          findings when unfit.
# EXIT:    0 fit · 1 unfit — the findings say why
set -euo pipefail

git_with_optional_timeout() {
	if command -v timeout >/dev/null 2>&1; then
		timeout -k 10 60 git "$@"
	else
		git "$@"
	fi
}

MINIMUM_FREE_SPACE_KIB=10485760

findings=0
origin_main_ready=0
report() {
	echo "$1"
	findings=$((findings + 1))
}

if ! git rev-parse --git-dir >/dev/null 2>&1; then
	report "$(pwd -P) is not a Git repository"
	exit 1
fi

if [ -z "$(git status --porcelain)" ]; then
	echo "working tree is clean"
else
	echo "working tree has uncommitted changes"
fi

if ! git remote get-url origin >/dev/null 2>&1; then
	report "no origin remote"
else
	fetch_status=0
	git_with_optional_timeout fetch --quiet origin >/dev/null 2>&1 || fetch_status=$?
	if [ "$fetch_status" -eq 124 ] || [ "$fetch_status" -eq 137 ]; then
		fetch_status=0
		git_with_optional_timeout fetch --quiet origin >/dev/null 2>&1 || fetch_status=$?
	fi
	if [ "$fetch_status" -ne 0 ]; then
		if [ "$fetch_status" -eq 124 ] || [ "$fetch_status" -eq 137 ]; then
			report "git fetch origin timed out"
		else
			report "git fetch origin failed"
		fi
	elif ! git rev-parse --verify --quiet origin/main >/dev/null; then
		report "origin/main does not resolve; fetch has never succeeded here"
	else
		origin_main_ready=1
		behind=$(git rev-list --count HEAD..origin/main)
		ahead=$(git rev-list --count origin/main..HEAD)
		if [ "$behind" -gt 0 ]; then
			echo "checkout is $behind commits behind origin/main; newest undeployed subject: $(git log -1 --format=%s HEAD..origin/main)"
		fi
		if [ "$ahead" -gt 0 ]; then
			report "checkout is $ahead commits ahead of origin/main"
		elif [ "$behind" -eq 0 ]; then
			echo "checkout is current with origin/main"
		fi
	fi
fi

# The contract zone: something reads each of these, so each has rules. A
# missing folder is reported and its shape is then not inspected, because
# every finding below would repeat the same fact.
contract_zone_present=1
for folder in project scripts tickets records; do
	if [ ! -d "sdlc/$folder" ]; then
		report "sdlc/$folder/ is missing"
		contract_zone_present=0
	fi
done

# A legacy issues folder is optional while existing repositories drain it.
# Only *.md is a contract artifact; .gitkeep and anything else a project
# keeps beside them is not this check's business.
artifact_names() {
	find "$1" -maxdepth 1 -type f -name '*.md' -exec basename {} \; 2>/dev/null | sort
}

if [ "$contract_zone_present" -eq 1 ]; then
	ticket_numbers=""
	record_numbers=""
	legacy_issue_numbers=""

	for folder in sdlc/tickets sdlc/tickets/drafts sdlc/tickets/archive sdlc/records sdlc/issues; do
		[ -d "$folder" ] || continue
		for name in $(artifact_names "$folder"); do
			case "$folder" in
			sdlc/issues)
				# Classify dates first: their year prefix must not reserve a number.
				case "$name" in
				[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-*.md) ;;
				[0-9][0-9][0-9][0-9]-*.md)
					number=${name%%-*}
					legacy_issue_numbers="$legacy_issue_numbers $number"
					;;
				*)
					echo "$folder/$name is not YYYY-MM-DD-short-slug.md or NNNN-name.md"
					;;
				esac
				;;
			*)
				case "$name" in
				[0-9][0-9][0-9][0-9]-*.md) ;;
				*)
					report "$folder/$name is not NNNN-name.md"
					continue
					;;
				esac
				number=${name%%-*}
				case "$folder" in
				sdlc/records) record_numbers="$record_numbers $number" ;;
				*) ticket_numbers="$ticket_numbers $number" ;;
				esac
				;;
			esac
		done
	done

	# Legacy issues still reserve a counter number. Records share a number with
	# their tickets, so they only collide when a legacy issue uses that number.
	duplicates=$(
		{
			printf '%s\n' $ticket_numbers $legacy_issue_numbers | sort | uniq -d
			for number in $record_numbers; do
				if printf '%s\n' $legacy_issue_numbers | grep -qx "$number"; then
					printf '%s\n' "$number"
				fi
			done
		} | sort -u
	)
	for number in $duplicates; do
		report "number $number is used more than once across numbered artifacts"
	done

	# A record whose ticket is absent is NOT reported. Repositories differ on
	# what happens to a landed ticket — most archive it, some delete it — and
	# this script cannot tell a deliberately deleted ticket from a lost one.
	# Reporting the difference would mark a healthy repository unfit for
	# following its own convention.
fi

# Reuse the final build gate's declaration check rather than maintaining a
# second list of permitted opening prefixes. Draft and archive tickets are not
# dispatchable, so only direct children of sdlc/tickets are checked.
path_policy=assembly/flows/build/05-verify/gate/05-path-policy
if [ "$origin_main_ready" -eq 1 ] && [ -f "$path_policy" ]; then
	while IFS= read -r ticket_ref; do
		case "$ticket_ref" in
		sdlc/tickets/[0-9][0-9][0-9][0-9]-*.md)
			ticket_id=${ticket_ref#sdlc/tickets/}
			ticket_id=${ticket_id%%-*}
			if ! diagnostic=$(TICKET_ID="$ticket_id" TICKET_REF="$ticket_ref" PATH_POLICY_VALIDATE_DECLARATION=1 sh "$path_policy" 2>&1); then
				report "$ticket_ref: $diagnostic"
			fi
			;;
		esac
	done < <(git ls-tree --name-only origin/main -- sdlc/tickets/)
fi

registered_directory=$(pwd -P)
if ! disk_report=$(df -Pk "$registered_directory"); then
	report "could not determine free space for $registered_directory"
else
	parsed=$(printf '%s\n' "$disk_report" | awk 'NR == 2 { print $4, $6 }')
	read -r available mount <<EOF
$parsed
EOF
	if [ -z "$available" ] || [ -z "$mount" ]; then
		report "could not determine free space for $registered_directory"
	else
		case "$available" in
		*[!0-9]*) report "could not determine free space for $registered_directory" ;;
		*)
			if [ "$available" -lt "$MINIMUM_FREE_SPACE_KIB" ]; then
				report "$mount has $available KiB available; floor is $MINIMUM_FREE_SPACE_KIB KiB"
			fi
			;;
		esac
	fi
fi

project_health=sdlc/scripts/health
if [ -x "$project_health" ] && ! "$project_health"; then
	findings=$((findings + 1))
fi

[ "$findings" -eq 0 ] || exit 1
