#!/usr/bin/env bash
#
# zrec - Interactive record inserter for recutils recfiles
# Compatible with bash 3.2.57 (macOS)
#
# Usage: zrec <recfile> [record-type]
#

TAB=$(printf '\t')

set -o pipefail

# ---------------------------------------------------------------------------
# Step 0: Verify recutils (recins/recsel) is installed
# ---------------------------------------------------------------------------
check_recutils() {
	if ! command -v recins >/dev/null 2>&1; then
		echo "Error: 'recins' not found. Please install recutils." >&2
		echo "  On macOS:  brew install recutils" >&2
		exit 1
	fi
	if ! command -v recinf >/dev/null 2>&1; then
		echo "Error: 'recinf' not found. Please install recutils." >&2
		exit 1
	fi

	local ver
	ver=$(recins --version 2>/dev/null | head -n1)
	echo "Using: ${ver:-recutils (version unknown)}"
}

# ---------------------------------------------------------------------------
# Step 1: Usage / argument handling
# ---------------------------------------------------------------------------
usage() {
	echo "Usage: $(basename "$0") <recfile> [record-type]" >&2
	echo "  Interactively build and insert a record into a recfile." >&2
	exit 1
}

# ---------------------------------------------------------------------------
# Step 2: Extract the descriptor block for a given record type.
# ---------------------------------------------------------------------------
get_descriptor_block() {
	local file="$1"
	local rectype="$2"

	awk -v rt="$rectype" '
        BEGIN { RS=""; FS="\n" }
        {
            for (i = 1; i <= NF; i++) {
                if ($i ~ /^%rec:[[:space:]]*/) {
                    name = $i
                    sub(/^%rec:[[:space:]]*/, "", name)
                    sub(/[[:space:]]+$/, "", name)
                    if (name == rt) {
                        print $0
                        exit
                    }
                }
            }
        }
    ' "$file"
}

# ---------------------------------------------------------------------------
# Step 3: List all record types defined in the file.
# ---------------------------------------------------------------------------
list_record_types() {
	local file="$1"

	if command -v recinf >/dev/null 2>&1; then
		recinf "$file" 2>/dev/null | awk '
            {
                if (NF == 0) next
                name = $NF
                if ($1 ~ /^[0-9]+$/) {
                    printf "%s [count: %s]\n", name, $1
                } else {
                    print name
                }
            }
        '
	else
		grep '^%rec:' "$file" | sed 's/^%rec:[[:space:]]*//; s/[[:space:]]*$//'
	fi
}

# ---------------------------------------------------------------------------
# Step 4: Pull a space-separated list of fields from a directive.
# ---------------------------------------------------------------------------
extract_fields() {
	local block="$1"
	local directive="$2"

	echo "$block" | awk -v d="$directive" '
        index($0, d) == 1 {
            line = substr($0, length(d) + 1)
            print line
        }
    ' | tr '\n' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}

# ---------------------------------------------------------------------------
# Step 5a: Resolve a named typedef back to its base type string.
#
# Handles two typedef forms:
#
#   %typedef: phonenum regexp /pattern/
#       -> returns "regexp /pattern/"
#
#   %typedef: score range 1 100
#       -> returns "range 1 100"
# ---------------------------------------------------------------------------
resolve_typedef() {
	local block="$1"
	local alias="$2"

	echo "$block" | awk -v a="$alias" '
        index($0, "%typedef:") == 1 {
            rest = substr($0, length("%typedef:") + 1)
            sub(/^[[:space:]]+/, "", rest)

            # parts[1] = alias name, parts[2..] = type definition tokens
            n = split(rest, parts, /[[:space:]]+/)
            if (parts[1] != a) next

            # Reassemble the definition (everything after the alias name).
            defn = ""
            for (i = 2; i <= n; i++) {
                defn = defn (i==2 ? "" : " ") parts[i]
            }

            print defn
            exit
        }
    '
}

# ---------------------------------------------------------------------------
# Step 5b: Find the declared type (if any) for a specific field.
#         Resolves %typedef: aliases to their underlying type string.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Look up a field's type ONCE and return two tab-separated values:
#
#       <resolved-type><TAB><display-label>
#
#   - resolved-type : the fully-resolved type string used for VALIDATION
#                     (e.g. "regexp /^.../", "int", "range 1 100").
#   - display-label : a human-friendly label used for the PROMPT
#                     (e.g. the alias "phonenum", or "regexp" for an
#                     inline pattern, or the base type name).
#
#   Both fields may be empty if the field has no declared type.
#   This performs a single awk pass over the descriptor block.
# ---------------------------------------------------------------------------
get_field_type_info() {
	local block="$1"
	local field="$2"

	# Single awk pass: pull the raw %type: token(s) for this field.
	local raw_type
	raw_type=$(echo "$block" | awk -v f="$field" '
        index($0, "%type:") == 1 {
            rest = substr($0, length("%type:") + 1)
            sub(/^[[:space:]]+/, "", rest)
            n = split(rest, parts, /[[:space:]]+/)
            names = parts[1]
            typedef = ""
            for (i = 2; i <= n; i++) {
                typedef = typedef (i==2 ? "" : " ") parts[i]
            }
            m = split(names, namearr, /,/)
            for (j = 1; j <= m; j++) {
                if (namearr[j] == f) {
                    print typedef
                    exit
                }
            }
        }
    ')

	# No declared type: emit an empty resolved/label pair.
	if [ -z "$raw_type" ]; then
		printf '\t'
		return 0
	fi

	local base="${raw_type%% *}"
	local resolved label

	case "$base" in
	int | real | bool | date | line | url | email | uuid | regexp | range)
		# Concrete inline type.
		resolved="$raw_type"
		# Hide the regexp pattern in the label; show just the keyword.
		label="$base"
		;;
	*)
		# Named alias: resolve it (one typedef lookup, only when needed).
		local typedef
		typedef=$(resolve_typedef "$block" "$base")
		if [ -n "$typedef" ]; then
			resolved="$typedef" # validate against the underlying type
			label="$base"       # but display the friendly alias name
		else
			# Unknown alias: fall back to raw for both.
			resolved="$raw_type"
			label="$raw_type"
		fi
		;;
	esac

	printf '%s\t%s' "$resolved" "$label"
}

# ---------------------------------------------------------------------------
# Helper: test membership of a word in a space-separated list.
# ---------------------------------------------------------------------------
in_list() {
	local needle="$1"
	shift
	local item
	for item in "$@"; do
		[ "$item" = "$needle" ] && return 0
	done
	return 1
}

# ---------------------------------------------------------------------------
# ### NEW: Step 6a: Validate (and where possible normalise) a value against
# a recutils field type string.  Prints the (possibly normalised) value on
# stdout and returns 0 on success, or prints an error to stderr and returns
# 1 on failure.
#
# Supported types:
#   int                        - integer
#   real                       - floating-point number
#   bool                       - yes/no/true/false/0/1  (normalised to "yes"/"no")
#   date                       - ISO-8601 or free date accepted by `date`
#   line                       - single line (no embedded newlines – always valid)
#   url                        - must start with a recognised scheme
#   email                      - must match user@domain
#   uuid                       - must match 8-4-4-4-12 hex pattern
#   regexp /pattern/           - value must match the given ERE
#   range [min] [max]          - min, max, or both may be omitted
# ---------------------------------------------------------------------------
validate_field_type() {
	local value="$1"
	local ftype="$2"

	local base_type="${ftype%% *}"

	case "$base_type" in

	int)
		if printf '%s' "$value" | grep -qE '^-?[0-9]+$'; then
			printf '%s' "$value"
			return 0
		fi
		echo "  -> Expected an integer (e.g. 42, -7)." >&2
		return 1
		;;

	real)
		if printf '%s' "$value" | grep -qE '^-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?$'; then
			printf '%s' "$value"
			return 0
		fi
		echo "  -> Expected a real number (e.g. 3.14, -2.0, 1e10)." >&2
		return 1
		;;

	bool)
		case "$value" in
		1 | [Yy][Ee][Ss] | [Tt][Rr][Uu][Ee])
			printf 'yes'
			return 0
			;;
		0 | [Nn][Oo] | [Ff][Aa][Ll][Ss][Ee])
			printf 'no'
			return 0
			;;
		esac
		echo "  -> Expected a boolean: yes/no, true/false, or 1/0." >&2
		return 1
		;;

	date)
		if date -d "$value" >/dev/null 2>&1 ||
			date -j -f "%Y-%m-%d" "$value" "+%Y-%m-%d" >/dev/null 2>&1; then
			printf '%s' "$value"
			return 0
		fi
		echo "  -> Expected a valid date (e.g. 2024-07-04)." >&2
		return 1
		;;

	line)
		printf '%s' "$value"
		return 0
		;;

	url)
		if printf '%s' "$value" |
			grep -qiE '^(https?|ftp|ftps|sftp|file)://[^[:space:]]+$'; then
			printf '%s' "$value"
			return 0
		fi
		echo "  -> Expected a URL beginning with http://, https://, ftp://, etc." >&2
		return 1
		;;

	email)
		if printf '%s' "$value" | grep -qE '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$'; then
			printf '%s' "$value"
			return 0
		fi
		echo "  -> Expected an e-mail address (e.g. user@example.com)." >&2
		return 1
		;;

	uuid)
		if printf '%s' "$value" |
			grep -qiE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
			printf '%s' "$value"
			return 0
		fi
		echo "  -> Expected a UUID (e.g. 550e8400-e29b-41d4-a716-446655440000)." >&2
		return 1
		;;

	regexp)
		local pattern="${ftype#regexp }"
		pattern="${pattern#/}"
		pattern="${pattern%/}"

		# ### CHANGED: Guard against empty pattern before attempting match.
		if [ -z "$pattern" ]; then
			echo "  -> Warning: empty regexp in type declaration; skipping validation." >&2
			printf '%s' "$value"
			return 0
		fi

		# ### CHANGED: Use awk for ERE matching instead of grep -E for
		# consistent behaviour across BSD (macOS) and GNU/Linux platforms.
		if printf '%s' "$value" |
			awk -v pat="$pattern" '$0 ~ pat { found=1 } END { exit !found }'; then
			printf '%s' "$value"
			return 0
		fi
		echo "  -> Value does not match required pattern: /${pattern}/." >&2
		return 1
		;;

	range)
		if ! printf '%s' "$value" | grep -qE '^-?[0-9]+(\.[0-9]+)?$'; then
			echo "  -> Expected a number for a range field." >&2
			return 1
		fi

		local lo hi
		lo=$(echo "$ftype" | awk '{print $2}')
		hi=$(echo "$ftype" | awk '{print $3}')

		if [ -n "$lo" ] &&
			awk -v v="$value" -v lo="$lo" 'BEGIN { exit !(v < lo) }'; then
			echo "  -> Value must be >= ${lo}." >&2
			return 1
		fi
		if [ -n "$hi" ] &&
			awk -v v="$value" -v hi="$hi" 'BEGIN { exit !(v > hi) }'; then
			echo "  -> Value must be <= ${hi}." >&2
			return 1
		fi
		printf '%s' "$value"
		return 0
		;;

	*)
		printf '%s' "$value"
		return 0
		;;
	esac
}

# ---------------------------------------------------------------------------
# Step 6: Prompt for a single field's value.
#   Returns the (possibly normalised) value via stdout.
#   Mandatory fields loop until non-empty; typed fields loop until valid.
# ---------------------------------------------------------------------------
prompt_field() {
	local field="$1"
	local is_mandatory="$2" # "yes" / "no"
	local ftype="$3"        # resolved type, used for validation
	local type_label="$4"   # friendly label, used for display

	local label="$field"
	local tags=""
	[ "$is_mandatory" = "yes" ] && tags="${tags} [MANDATORY]"
	[ -n "$type_label" ] && tags="${tags} (type: ${type_label})"

	local value validated
	while true; do
		printf "%s%s: " "$label" "$tags" >&2
		IFS= read -r value

		# Mandatory-empty check.
		if [ "$is_mandatory" = "yes" ] && [ -z "$value" ]; then
			echo "  -> This field is mandatory and cannot be empty." >&2
			continue
		fi

		# Skip type validation for empty optional fields.
		if [ -z "$value" ]; then
			printf '%s' "$value"
			return 0
		fi

		# ### CHANGED: If a type is declared, validate (and possibly normalise)
		# the input; re-prompt on failure.
		if [ -n "$ftype" ]; then
			if validated=$(validate_field_type "$value" "$ftype"); then
				printf '%s' "$validated"
			else
				continue
			fi
		else
			printf '%s' "$value"
		fi
		return 0
	done
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
	check_recutils

	[ "$#" -ge 1 ] || usage
	local recfile="$1"
	local rectype="$2"

	if [ ! -f "$recfile" ]; then
		echo "Error: recfile '$recfile' does not exist." >&2
		exit 1
	fi

	# ---- Determine record type --------------------------------------------
	local available_types
	available_types=$(list_record_types "$recfile")

	if [ -z "$rectype" ]; then
		if [ -z "$available_types" ]; then
			echo "No %rec: types found. This recfile uses free-form fields." >&2
		else
			local -a display_list
			local -a name_list
			display_list=()
			name_list=()

			local _line
			while IFS= read -r _line; do
				[ -z "$_line" ] && continue
				display_list+=("$_line")
				local plain="${_line%% \[count:*}"
				name_list+=("$plain")
			done <<EOF
$available_types
EOF

			local count="${#name_list[@]}"

			if [ "$count" -eq 1 ]; then
				local only_type="${name_list[0]}"
				echo "Only one record type found: '$only_type'" >&2
				printf "Press Enter to use it, or type a different type: " >&2
				IFS= read -r rectype
				[ -z "$rectype" ] && rectype="$only_type"

			else
				# ----- Multiple types: single-keypress numbered menu -------
				echo "Available record types:" >&2
				local i=1
				local label
				for label in "${display_list[@]}"; do
					printf "  %d) %s\n" "$i" "$label" >&2
					i=$((i + 1))
				done

				while true; do
					# ### CHANGED: -n1 reads exactly one character without
					# requiring Enter.  -s suppresses echo so we can echo
					# the digit ourselves on the same line for clarity.
					printf "Select a record type (1-%d): " "$count" >&2
					local choice
					IFS= read -r -n1 -s choice
					# Echo the character the user pressed so they can see it.
					printf '%s\n' "$choice" >&2

					# Blank (Enter pressed directly) → re-prompt.
					if [ -z "$choice" ]; then
						echo "  -> Please press a digit between 1 and $count." >&2
						continue
					fi

					# Reject non-digits immediately.
					case "$choice" in
					*[!0-9]*)
						echo "  -> Please press a digit between 1 and $count." >&2
						continue
						;;
					esac

					if [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then
						rectype="${name_list[$((choice - 1))]}"
						break
					else
						echo "  -> Out of range. Choose between 1 and $count." >&2
					fi
				done
			fi

			echo "Selected record type: $rectype" >&2
		fi
	fi

	# ---- Load the descriptor block ----------------------------------------
	local block=""
	if [ -n "$rectype" ]; then
		block=$(get_descriptor_block "$recfile" "$rectype")
		if [ -z "$block" ]; then
			echo "Warning: no descriptor block found for type '$rectype'." >&2
		fi
	fi

	# ---- Gather field rules -----------------------------------------------
	local mandatory_fields allowed_fields
	mandatory_fields=$(extract_fields "$block" "%mandatory:")
	allowed_fields=$(extract_fields "$block" "%allowed:")

	echo "" >&2
	echo "----- Building a new record -----" >&2
	[ -n "$rectype" ] && echo "Record type: $rectype" >&2
	echo "" >&2

	local -a args
	args=()

	local prompted=""

	# ---- 1. Prompt for ALLOWED fields (if defined) ------------------------
	if [ -n "$allowed_fields" ]; then
		echo "== Allowed fields ==" >&2
		local f
		for f in $allowed_fields; do
			# ### CHANGED: Skip the %rec: type-name field.  When -t is passed
			# to recins it writes the %rec: line itself; supplying it again as
			# a -f/-v pair produces a malformed record with a duplicate or
			# misplaced %rec: field.
			if [ -n "$rectype" ] && [ "$f" = "$rectype" ]; then
				continue
			fi

			local mand="no"
			in_list "$f" $mandatory_fields && mand="yes"
			local info ftype label
			info=$(get_field_type_info "$block" "$f")
			ftype="${info%%"$TAB"*}" # everything before the tab  -> resolved type
			label="${info#*"$TAB"}"  # everything after the tab   -> display label

			local val
			val=$(prompt_field "$f" "$mand" "$ftype" "$label")
			if [ -n "$val" ]; then
				args+=(-f "$f" -v "$val")
			fi
			prompted="$prompted $f"
		done
	fi

	# ---- 2. Prompt for any MANDATORY fields not in the allowed list -------
	if [ -n "$mandatory_fields" ]; then
		local f
		local printed_header="no"
		for f in $mandatory_fields; do
			if ! in_list "$f" $prompted; then
				if [ "$printed_header" = "no" ]; then
					echo "== Mandatory fields ==" >&2
					printed_header="yes"
				fi
				local info ftype label
				info=$(get_field_type_info "$block" "$f")
				ftype="${info%%"$TAB"*}" # everything before the tab  -> resolved type
				label="${info#*"$TAB"}"  # everything after the tab   -> display label

				local val
				val=$(prompt_field "$f" "yes" "$ftype" "$label")
				args+=(-f "$f" -v "$val")
				prompted="$prompted $f"
			fi
		done
	fi

	# ---- 3. Prompt for free-form / user-defined fields --------------------
	# ### CHANGED: Only offer free-form entry when no %allowed: constraint
	# exists for this record type.  If %allowed: is defined, recins will
	# reject any field not listed there, so presenting the prompt would
	# only allow the user to produce a record that fails on insert.
	if [ -z "$allowed_fields" ]; then
		echo "" >&2
		echo "== Additional fields ==" >&2
		echo "Enter a field name (or press Enter / type 'done' to finish):" >&2
		while true; do
			printf "Field name: " >&2
			local newfield
			IFS= read -r newfield
			case "$newfield" in
			"" | done | DONE | q | quit) break ;;
			esac

			local info ftype label
			info=$(get_field_type_info "$block" "$newfield")
			ftype="${info%%"$TAB"*}" # everything before the tab  -> resolved type
			label="${info#*"$TAB"}"  # everything after the tab   -> display label

			local val
			val=$(prompt_field "$newfield" "no" "$ftype" "$label")

			args+=(-f "$newfield" -v "$val")
		done
	else
		echo "" >&2
		echo "== Additional fields ==" >&2
		echo "(Skipped: this record type has %allowed: constraints.)" >&2
	fi

	# ---- Sanity check: did we collect anything? ---------------------------
	if [ "${#args[@]}" -eq 0 ]; then
		echo "No fields entered. Aborting." >&2
		exit 1
	fi

	# ---- Build and run recins ---------------------------------------------
	echo "" >&2
	echo "Inserting record..." >&2

	local status
	if [ -n "$rectype" ]; then
		recins -t "$rectype" "${args[@]}" "$recfile"
		status=$?
	else
		recins "${args[@]}" "$recfile"
		status=$?
	fi

	if [ "$status" -eq 0 ]; then
		echo "Record inserted successfully into '$recfile'." >&2
	else
		echo "recins failed (exit code $status)." >&2
		exit "$status"
	fi

}

main "$@"
