#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
# shellcheck disable=SC2329
# =============================================================================
# Generate Agent Skills SKILL.md Files
# =============================================================================
# Creates SKILL.md index files for Agent Skills compatibility while maintaining
# the aidevops pattern of {name}.md + {name}/ folder structure.
#
# This script generates lightweight SKILL.md stubs that reference the actual
# content in existing .md files, enabling cross-tool compatibility with:
# - Any tool supporting the Agent Skills standard (agentskills.io)
#
# Pattern:
#   wordpress.md + wordpress/ → wordpress/SKILL.md (generated)
#   wordpress/wp-dev.md → wordpress/wp-dev/SKILL.md (generated)
#
# Usage:
#   ./generate-skills.sh [--dry-run] [--clean] [--verbose]
#
# Options:
#   --dry-run  Show what would be generated without writing files
#   --clean    Remove all generated SKILL.md files
#   --verbose  Show per-file output (suppressed by default in normal mode)
# =============================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit
source "${SCRIPT_DIR}/shared-constants.sh"

set -euo pipefail

# Configuration
AGENTS_DIR="${AIDEVOPS_AGENTS_DIR:-$HOME/.aidevops/agents}"
DRY_RUN=false
CLEAN=false
VERBOSE=false
SOURCE_MD_GLOB="*.md"
SKILL_MD_NAME="SKILL.md"
AGENTS_MD_NAME="AGENTS.md"
README_MD_NAME="README.md"
MARKDOWN_H1_PREFIX="# "

# Parse arguments
while [[ $# -gt 0 ]]; do
	case $1 in
	--dry-run)
		DRY_RUN=true
		shift
		;;
	--clean)
		CLEAN=true
		shift
		;;
	--verbose | -v)
		VERBOSE=true
		shift
		;;
	*)
		echo -e "${RED}Unknown option: $1${NC}"
		exit 1
		;;
	esac
done

# =============================================================================
# Helper Functions
# =============================================================================

log_info() {
	echo -e "${BLUE}$1${NC}"
	return 0
}

log_success() {
	echo -e "${GREEN}✓${NC} $1"
	return 0
}

log_warning() {
	echo -e "${YELLOW}⚠${NC} $1"
	return 0
}

log_error() {
	echo -e "${RED}✗${NC} $1"
	return 0
}

is_verbose_output() {
	if [[ "$VERBOSE" == true || "$DRY_RUN" == true ]]; then
		return 0
	fi

	return 1
}

# Log per-file success only in verbose or dry-run mode
log_file_success() {
	if is_verbose_output; then
		echo -e "${GREEN}✓${NC} $1"
	fi
	return 0
}

# Extract frontmatter field from markdown file
extract_frontmatter_field() {
	local file="$1"
	local field="$2"
	local line=""
	local in_frontmatter=false
	EXTRACT_FRONTMATTER_FIELD_RESULT=""

	if [[ ! -f "$file" ]]; then
		return 1
	fi

	# Extract value between initial --- frontmatter markers without forking awk
	# once per file. Skill generation may inspect hundreds of markdown files
	# during setup, so avoiding per-file subprocesses keeps non-interactive setup
	# within its bounded postflight window.
	while IFS= read -r line; do
		if [[ "$line" == "---" ]]; then
			if [[ "$in_frontmatter" == true ]]; then
				break
			fi
			in_frontmatter=true
			continue
		fi

		if [[ "$in_frontmatter" == true && "$line" == "$field:"* ]]; then
			line="${line#"${field}":}"
			line="${line#"${line%%[![:space:]]*}"}"
			line="${line%\"}"
			line="${line#\"}"
			line="${line%\'}"
			line="${line#\'}"
			EXTRACT_FRONTMATTER_FIELD_RESULT="$line"
			return 0
		fi
	done <"$file"
	return 0
}

# Extract description from file - tries frontmatter first, then first heading
extract_description() {
	local file="$1"
	local desc=""
	EXTRACT_DESCRIPTION_RESULT=""

	# Try frontmatter first
	extract_frontmatter_field "$file" "description"
	desc="$EXTRACT_FRONTMATTER_FIELD_RESULT"
	if [[ -n "$desc" ]]; then
		EXTRACT_DESCRIPTION_RESULT="$desc"
		return 0
	fi

	# Try first heading (# Title - Description pattern or just # Title)
	local heading=""
	local line=""
	while IFS= read -r line; do
		if [[ "$line" == "$MARKDOWN_H1_PREFIX"* ]]; then
			heading="${line#"$MARKDOWN_H1_PREFIX"}"
			break
		fi
	done <"$file"
	if [[ -n "$heading" ]]; then
		# If heading has " - ", take the part after
		if [[ "$heading" == *" - "* ]]; then
			EXTRACT_DESCRIPTION_RESULT="${heading#* - }"
		else
			EXTRACT_DESCRIPTION_RESULT="$heading"
		fi
		return 0
	fi

	# Fallback to filename
	local fallback="${file##*/}"
	EXTRACT_DESCRIPTION_RESULT="${fallback%.md} skill"
	return 0
}

# Lowercase one ASCII character without spawning tr. Bash 3.2 does not support
# ${var,,}, and skill generation calls this path thousands of times during
# setup, so avoiding per-call subprocesses is material on macOS.
lowercase_ascii_char() {
	local char="$1"
	case "$char" in
	A) LOWERCASE_ASCII_CHAR_RESULT='a' ;;
	B) LOWERCASE_ASCII_CHAR_RESULT='b' ;;
	C) LOWERCASE_ASCII_CHAR_RESULT='c' ;;
	D) LOWERCASE_ASCII_CHAR_RESULT='d' ;;
	E) LOWERCASE_ASCII_CHAR_RESULT='e' ;;
	F) LOWERCASE_ASCII_CHAR_RESULT='f' ;;
	G) LOWERCASE_ASCII_CHAR_RESULT='g' ;;
	H) LOWERCASE_ASCII_CHAR_RESULT='h' ;;
	I) LOWERCASE_ASCII_CHAR_RESULT='i' ;;
	J) LOWERCASE_ASCII_CHAR_RESULT='j' ;;
	K) LOWERCASE_ASCII_CHAR_RESULT='k' ;;
	L) LOWERCASE_ASCII_CHAR_RESULT='l' ;;
	M) LOWERCASE_ASCII_CHAR_RESULT='m' ;;
	N) LOWERCASE_ASCII_CHAR_RESULT='n' ;;
	O) LOWERCASE_ASCII_CHAR_RESULT='o' ;;
	P) LOWERCASE_ASCII_CHAR_RESULT='p' ;;
	Q) LOWERCASE_ASCII_CHAR_RESULT='q' ;;
	R) LOWERCASE_ASCII_CHAR_RESULT='r' ;;
	S) LOWERCASE_ASCII_CHAR_RESULT='s' ;;
	T) LOWERCASE_ASCII_CHAR_RESULT='t' ;;
	U) LOWERCASE_ASCII_CHAR_RESULT='u' ;;
	V) LOWERCASE_ASCII_CHAR_RESULT='v' ;;
	W) LOWERCASE_ASCII_CHAR_RESULT='w' ;;
	X) LOWERCASE_ASCII_CHAR_RESULT='x' ;;
	Y) LOWERCASE_ASCII_CHAR_RESULT='y' ;;
	Z) LOWERCASE_ASCII_CHAR_RESULT='z' ;;
	*) LOWERCASE_ASCII_CHAR_RESULT="$char" ;;
	esac
	return 0
}

# Uppercase one ASCII character without spawning tr/cut.
uppercase_ascii_char() {
	local char="$1"
	case "$char" in
	a) UPPERCASE_ASCII_CHAR_RESULT='A' ;;
	b) UPPERCASE_ASCII_CHAR_RESULT='B' ;;
	c) UPPERCASE_ASCII_CHAR_RESULT='C' ;;
	d) UPPERCASE_ASCII_CHAR_RESULT='D' ;;
	e) UPPERCASE_ASCII_CHAR_RESULT='E' ;;
	f) UPPERCASE_ASCII_CHAR_RESULT='F' ;;
	g) UPPERCASE_ASCII_CHAR_RESULT='G' ;;
	h) UPPERCASE_ASCII_CHAR_RESULT='H' ;;
	i) UPPERCASE_ASCII_CHAR_RESULT='I' ;;
	j) UPPERCASE_ASCII_CHAR_RESULT='J' ;;
	k) UPPERCASE_ASCII_CHAR_RESULT='K' ;;
	l) UPPERCASE_ASCII_CHAR_RESULT='L' ;;
	m) UPPERCASE_ASCII_CHAR_RESULT='M' ;;
	n) UPPERCASE_ASCII_CHAR_RESULT='N' ;;
	o) UPPERCASE_ASCII_CHAR_RESULT='O' ;;
	p) UPPERCASE_ASCII_CHAR_RESULT='P' ;;
	q) UPPERCASE_ASCII_CHAR_RESULT='Q' ;;
	r) UPPERCASE_ASCII_CHAR_RESULT='R' ;;
	s) UPPERCASE_ASCII_CHAR_RESULT='S' ;;
	t) UPPERCASE_ASCII_CHAR_RESULT='T' ;;
	u) UPPERCASE_ASCII_CHAR_RESULT='U' ;;
	v) UPPERCASE_ASCII_CHAR_RESULT='V' ;;
	w) UPPERCASE_ASCII_CHAR_RESULT='W' ;;
	x) UPPERCASE_ASCII_CHAR_RESULT='X' ;;
	y) UPPERCASE_ASCII_CHAR_RESULT='Y' ;;
	z) UPPERCASE_ASCII_CHAR_RESULT='Z' ;;
	*) UPPERCASE_ASCII_CHAR_RESULT="$char" ;;
	esac
	return 0
}

# Convert name to valid skill name (lowercase, hyphens only)
to_skill_name() {
	local name="$1"
	local out=""
	local char=""
	local last_dash=false
	local i
	LOWERCASE_ASCII_CHAR_RESULT=""
	TO_SKILL_NAME_RESULT=""

	for ((i = 0; i < ${#name}; i++)); do
		lowercase_ascii_char "${name:i:1}"
		char="$LOWERCASE_ASCII_CHAR_RESULT"
		case "$char" in
		[a-z0-9])
			out+="$char"
			last_dash=false
			;;
		*)
			if [[ "$last_dash" == false ]]; then
				out+="-"
				last_dash=true
			fi
			;;
		esac
	done
	out="${out#-}"
	out="${out%-}"
	TO_SKILL_NAME_RESULT="$out"
	return 0
}

# Capitalize first letter (portable)
capitalize() {
	local str="$1"
	local first=""
	local rest=""
	CAPITALIZE_RESULT=""
	if [[ -z "$str" ]]; then
		return 0
	fi
	UPPERCASE_ASCII_CHAR_RESULT=""
	uppercase_ascii_char "${str:0:1}"
	first="$UPPERCASE_ASCII_CHAR_RESULT"
	rest="${str:1}"
	CAPITALIZE_RESULT="${first}${rest}"
	return 0
}

# Generate SKILL.md content for a folder with parent .md (pure pointer)
generate_folder_skill() {
	local folder_path="$1"
	local parent_md="$2"
	local folder_name
	folder_name="${folder_path##*/}"
	local skill_name
	to_skill_name "$folder_name"
	skill_name="$TO_SKILL_NAME_RESULT"

	# Extract description from parent .md
	local description
	extract_description "$parent_md"
	description="$EXTRACT_DESCRIPTION_RESULT"

	# Generate pure pointer SKILL.md — no inlined subskill lists
	local title
	capitalize "$folder_name"
	title="$CAPITALIZE_RESULT"

	echo "---"
	echo "name: ${skill_name}"
	echo "description: ${description}"
	echo "---"
	echo ""
	printf '%s%s\n' "$MARKDOWN_H1_PREFIX" "$title"
	echo ""
	echo "See [${folder_name}.md](../${folder_name}.md) for full instructions."
	return 0
}

# Generate SKILL.md content for a leaf .md file
generate_leaf_skill() {
	local md_file="$1"
	local filename
	filename="${md_file##*/}"
	filename="${filename%.md}"
	local skill_name
	to_skill_name "$filename"
	skill_name="$TO_SKILL_NAME_RESULT"

	# Extract description
	local description
	extract_description "$md_file"
	description="$EXTRACT_DESCRIPTION_RESULT"

	# Get relative path to the .md file from the new folder
	local relative_path="../${filename}.md"

	local title
	capitalize "$filename"
	title="$CAPITALIZE_RESULT"

	echo "---"
	echo "name: ${skill_name}"
	echo "description: ${description}"
	echo "---"
	echo ""
	printf '%s%s\n' "$MARKDOWN_H1_PREFIX" "$title"
	echo ""
	echo "See [${filename}.md](${relative_path}) for full instructions."
	return 0
}

# =============================================================================
# Clean Mode
# =============================================================================

if [[ "$CLEAN" == true ]]; then
	log_info "Cleaning generated SKILL.md files..."

	count=0
	while IFS= read -r skill_file; do
		if [[ "$DRY_RUN" == true ]]; then
			log_warning "Would remove: $skill_file"
		else
			rm -f "$skill_file"
			log_file_success "Removed: $skill_file"
		fi
		((++count))
	done < <(find "$AGENTS_DIR" -name "SKILL.md" -type f 2>/dev/null)

	if [[ $count -eq 0 ]]; then
		log_info "No SKILL.md files found to clean"
	else
		log_info "Cleaned $count SKILL.md files"
	fi
	exit 0
fi

# =============================================================================
# Cache check — skip generation if source .md files haven't changed
# =============================================================================

CACHE_HASH_FILE="${AGENTS_DIR}/.skills-source-hash"

has_any_skill_file() {
	local skill_file=""
	while IFS= read -r skill_file; do
		[[ -n "$skill_file" ]] && return 0
	done < <(find "$AGENTS_DIR" -name "$SKILL_MD_NAME" -type f -print -quit 2>/dev/null)
	return 1
}

has_source_newer_than_cache() {
	local newer_file=""
	while IFS= read -r newer_file; do
		[[ -n "$newer_file" ]] && return 0
	done < <(find "$AGENTS_DIR" -name "$SOURCE_MD_GLOB" -not -name "$SKILL_MD_NAME" -not -name "$AGENTS_MD_NAME" \
		-not -name "$README_MD_NAME" -type f -newer "$CACHE_HASH_FILE" -print -quit 2>/dev/null)
	return 1
}

compute_source_hash() {
	# Hash the listing of all source .md files with their sizes and mtimes.
	# This is fast (~10ms for 1600 files) vs regenerating (~56s).
	# Uses _stat_translate_fmt + xargs for ARG_MAX safety.
	_stat_translate_fmt '%n %s %Y' || return 1
	find "$AGENTS_DIR" -name "$SOURCE_MD_GLOB" -not -name "$SKILL_MD_NAME" -not -name "$AGENTS_MD_NAME" \
		-not -name "$README_MD_NAME" -type f -print0 2>/dev/null |
		xargs -0 stat "$_STAT_FLAG" "$_STAT_FMT" 2>/dev/null |
		LC_ALL=C sort | shasum -a 256 | cut -d' ' -f1
	return 0
}

if [[ "$DRY_RUN" == false && "$CLEAN" == false ]]; then
	if [[ -f "$CACHE_HASH_FILE" ]]; then
		# Warm setup runs should skip in milliseconds. The full source hash is a
		# correctness fallback for changed trees, but computing it on every run can
		# dominate the 90s postflight budget on macOS deployed-agent trees.
		if has_any_skill_file && ! has_source_newer_than_cache; then
			log_info "Agent Skills SKILL.md files up to date (source unchanged) — skipping generation"
			exit 0
		fi

		current_hash=$(compute_source_hash)
		stored_hash=$(cat "$CACHE_HASH_FILE" 2>/dev/null || echo "")
		if [[ "$current_hash" == "$stored_hash" ]]; then
			# Verify at least one SKILL.md exists (handles first run after cache file created manually)
			if has_any_skill_file; then
				log_info "Agent Skills SKILL.md files up to date (source unchanged) — skipping generation"
				exit 0
			fi
		fi
	else
		current_hash=$(compute_source_hash)
	fi
fi

# =============================================================================
# Generate Mode
# =============================================================================

log_info "Generating Agent Skills SKILL.md files..."
log_info "Source: $AGENTS_DIR"

if [[ "$DRY_RUN" == true ]]; then
	log_warning "DRY RUN - no files will be written"
fi

generated=0
skipped=0

# Pattern 1: Folders with matching parent .md files
# e.g., wordpress.md + wordpress/ → wordpress/SKILL.md
if is_verbose_output; then
	log_info ""
	log_info "Pattern 1: Folders with parent .md files"
fi

while IFS= read -r folder; do
	folder_name="${folder##*/}"
	parent_md="$AGENTS_DIR/${folder_name}.md"
	skill_file="$folder/SKILL.md"

	# Skip special folders
	if [[ "$folder_name" == "scripts" || "$folder_name" == "memory" || "$folder_name" == "templates" ]]; then
		continue
	fi

	if [[ -f "$parent_md" ]]; then
		if [[ "$DRY_RUN" == true ]]; then
			log_file_success "Would generate: $skill_file (from $parent_md)"
		else
			mkdir -p "$folder"
			generate_folder_skill "$folder" "$parent_md" >"$skill_file"
			log_file_success "Generated: $skill_file"
		fi
		((++generated))
	fi
done < <(find "$AGENTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)

# Pattern 2: Nested folders without parent .md but with children
# e.g., tools/browser/ with playwright.md, etc.
if is_verbose_output; then
	log_info ""
	log_info "Pattern 2: Nested folders with child .md files"
fi

while IFS= read -r folder; do
	folder_name="${folder##*/}"
	skill_file="$folder/SKILL.md"

	# Skip if already handled or special
	if [[ -f "$skill_file" ]]; then
		continue
	fi

	# Check if folder has .md files without spawning subprocesses.
	# Iterating via glob avoids a find|wc -l subprocess invocation per directory,
	# which is expensive when scanning hundreds of nested directories on macOS.
	_has_md=false
	for _md_chk in "$folder"/*.md; do
		[[ -f "$_md_chk" ]] && { _has_md=true; break; }
	done
	if [[ "$_has_md" == true ]]; then
		to_skill_name "$folder_name"
		local_name="$TO_SKILL_NAME_RESULT"

		if [[ "$DRY_RUN" == true ]]; then
			log_file_success "Would generate: $skill_file (folder index)"
		else
			# Pure pointer — no inlined subskill lists
			capitalize "$folder_name"
			title="$CAPITALIZE_RESULT"
			{
				echo "---"
				echo "name: ${local_name}"
				echo "description: ${title} tools and utilities"
				echo "---"
				echo ""
				printf '%s%s\n' "$MARKDOWN_H1_PREFIX" "$title"
				echo ""
				echo "Browse the .md files in this directory for full instructions."
			} >"$skill_file"
			log_file_success "Generated: $skill_file"
		fi
		((++generated))
	fi
done < <(find "$AGENTS_DIR" -mindepth 2 -type d 2>/dev/null | sort)

# Pattern 3: Standalone .md files in nested dirs without matching folders
# e.g., services/hosting/local-hosting.md with no local-hosting/ folder
# These were previously missed, causing discovery gaps.
if is_verbose_output; then
	log_info ""
	log_info "Pattern 3: Standalone .md files without matching folders"
fi

while IFS= read -r md_file; do
	filename="${md_file##*/}"
	filename="${filename%.md}"
	parent_dir="${md_file%/*}"
	target_dir="${parent_dir}/${filename}"
	skill_file="${target_dir}/SKILL.md"

	# Skip if SKILL.md already exists (directory may exist after --clean)
	if [[ -f "$skill_file" ]]; then
		continue
	fi

	# Skip SKILL.md files, AGENTS.md, README.md, and other non-skill files
	if [[ "$filename" == "SKILL" || "$filename" == "AGENTS" || "$filename" == "README" ]]; then
		continue
	fi

	# Skip files in special top-level directories
	rel_path="${md_file#"$AGENTS_DIR"/}"
	top_dir="${rel_path%%/*}"
	if [[ "$top_dir" == "scripts" || "$top_dir" == "memory" || "$top_dir" == "templates" || "$top_dir" == "configs" || "$top_dir" == "prompts" || "$top_dir" == "plugins" ]]; then
		continue
	fi

	# Skip top-level .md files (Pattern 1 handles those via folders)
	if [[ "$parent_dir" == "$AGENTS_DIR" ]]; then
		continue
	fi

	if [[ "$DRY_RUN" == true ]]; then
		log_file_success "Would generate: $skill_file (standalone)"
	else
		mkdir -p "$target_dir"
		generate_leaf_skill "$md_file" >"$skill_file"
		log_file_success "Generated: $skill_file"
	fi
	((++generated))
done < <(find "$AGENTS_DIR" -mindepth 2 -name "$SOURCE_MD_GLOB" -not -name "$SKILL_MD_NAME" -not -name "$AGENTS_MD_NAME" -not -name "$README_MD_NAME" -type f 2>/dev/null | sort)

# =============================================================================
# Summary
# =============================================================================

log_info ""
log_info "Generation complete:"
log_info "  Generated: $generated SKILL.md files"
log_info "  Skipped: $skipped (already exist or excluded)"

if [[ "$DRY_RUN" == true ]]; then
	log_warning ""
	log_warning "This was a dry run. Run without --dry-run to generate files."
else
	# Write cache hash so next run skips if nothing changed.
	# The source set excludes generated SKILL.md files, so the pre-generation
	# hash is still valid after generation. Reusing it avoids running a second
	# find|sort pipeline after the visible "Generation complete" message, which
	# can leave setup.sh looking hung even though generation itself finished.
	new_hash="${current_hash:-}"
	if [[ -z "$new_hash" ]]; then
		new_hash=$(compute_source_hash)
	fi
	echo "$new_hash" >"$CACHE_HASH_FILE"
fi

exit 0
