#!/usr/bin/bash
#----------------------------------------------------------------------
#
#
#----------------------------------------------------------------------

CT_DIR=${CT_DIR:=/etc/pve/lxc/}

# XXX setup path...
# XXX


EDITOR=${EDITOR:-vim}



#----------------------------------------------------------------------
# XXX this is quite generic, might be a good idea to move this to a 
# 		seporate lib/file...

# Execute (optionally) and print a command.
#
#	@ COMMAND ARGS
#
#QUIET=
#DRY_RUN=
ECHO_PREFIX="### "
function @ (){
	if [ -z $DRY_RUN ] ; then
		! [ $QUIET ] \
			&& echo -e "${ECHO_PREFIX}$@"
		"$@"
	else
		echo -e $@
	fi
	return $?
}



#----------------------------------------------------------------------

#
#	check MSG COMMAND ..
#
check(){
	local MSG=$1
	shift
	for cmd in "$@" ; do
		which $cmd > /dev/null 2>&1 \
			|| eval "echo \"$MSG\"" >&2
	done
}

need(){
	check 'ERROR: "$cmd": needed by this script but not in path.' "$@"
}
would-like(){
	check 'WARNING: "$cmd": is not in path.' "$@"
}



#----------------------------------------------------------------------
# Fill section...
#
# XXX this is quite generic -- move to a more logical place...

fillsection(){ (
	usage(){
		echo "Usage:"
		echo "    ${FUNCNAME[0]} [-h]"
		echo "    ${FUNCNAME[0]} [-r] NAME FILE [CONTENT]"
		echo
	}
	while true ; do
		case $1 in
			-h|--help)
				usage
				echo "Options:"
				#	  .	  .					.
				echo "    -h | --help       print this help message and exit."
				echo "    -r | --return     replace section markers with CONTENT."
				echo
				return 0
				;;
			-r|--replace)
				local replace=1
				shift
				;;

			*)
				break
				;;
		esac
	done
	if [[ $# < 2 ]] ; then
		usage
		return 1
	fi

	name=$1
	file=$2
	content=$3
	content=${content:=/dev/stdin}

	# print file upto section marker...
	if [ $replace ] ; then
		sed "/${name^^} BEGIN/q" "$file" | sed '$d'
	else
		sed "/${name^^} BEGIN/q" "$file"
	fi
	# print content...
	cat $content
	# print file from section end marker...
	if [ $replace ] ; then
		sed -ne "/${name^^} END/,$ p" "$file" | sed '1d' 
	else
		sed -ne "/${name^^} END/,$ p" "$file"
	fi
) }



#----------------------------------------------------------------------
# CT hostname <-> CT id...

ct2hostname(){
	local ct=${CT_DIR}/${1}.conf
	local host=$(cat $ct \
		| grep -e '^\s*hostname:' \
		| head -1)
	echo ${host/hostname: /}
}

hostname2ct(){
	if [ -e "${CT_DIR}/${1}.conf" ] ; then
		echo $1
	fi
	local running=$2
	running=${running:=any}
	local ct
	local host
	for ct in "${CT_DIR}"/*.conf ; do
		host=$(cat $ct | grep hostname | head -1)
		host=${host/hostname: /}
		if [ "$host" = $1 ] ; then
			ct=${ct#${CT_DIR}}
			ct=${ct%.conf}
			ct=${ct#\/}
			# filter results if needed...
			if [ $running = "any" ] ; then
				echo $ct
			else
				local status=`pct status $ct`
				if [ "$running" = "${status/status: /}" ] ; then
					echo $ct
				fi
			fi
		fi
	done
}



#----------------------------------------------------------------------

normpath(){
	echo $1 \
		| sed \
			-e 's/\/\+/\//g' \
			-e 's/\/.\//\//g' \
			-e 's/[^\/]\+\/\.\.//g' \
			-e 's/\/\+/\//g' \
			-e 's/\/\.$/\//g'
}



#----------------------------------------------------------------------

#
#	xread [-n] MSG VAR
#
# This saves all user input variables to the $XREAD_VARS array.
xread(){
	local non_empty=
	if [[ $1 == '-n' ]] ; then
		shift
		local non_empty=1
	fi
	local prefix=
	if [ $SCRIPTING ] ; then
		prefix='# '
	fi
	# skip...
	if [[ "${!2}" == "SKIP" ]] \
			|| [[ "$(eval "echo \$DFL_$2")" == "SKIP" ]] ; then
		eval "$2="
		return
	fi
	if [ -z ${!2} ] ; then
		eval 'read -ep "'$prefix''$1'" -i "$DFL_'$2'" '${2}''
		XREAD_VARS+=(${2})
	fi
	if [ -z $non_empty ] ; then
		eval ''$2'=${'$2':=$DFL_'$2'}'
	fi
	[ $SCRIPTING ] \
		&& echo "$2=${!2}"
}

#
#	xreadYes MSG [VAR]
#
xreadYes(){
	if [ -z ${2} ] ; then
		local var=__LOCAL
		local __LOCAL
	else
		local var=${2}
		local mode=
	fi
	local prefix=
	if [ $SCRIPTING ] ; then
		prefix='# '
	fi
	# XXX check DFL_..???
	if [[ "${!var}" == "SKIP" ]] ; then
		eval "$var="
		return
	fi
	if [ -z ${!var} ] ; then
		if [ -z $(eval "echo \$DFL_${var}") ] ; then
			local yes=y
			local no=N
			local dfl=
		else
			local yes=Y
			local no=n
			local dfl=1
		fi
		eval 'read -ep "'$prefix''$1' ('$yes'/'$no') " '${var}''
		XREAD_VARS+=(${var})
		# normalize...
		eval "${var}=${!var,,}"
		if [[ "${!var}" == 'y' ]] ; then
			eval "${var}=1"
		elif [[ ${!var} == 'n' ]] ; then
			eval "${var}="
		# set default if empty...
		else
			eval "${var}=\${${var}:-$dfl}"
		fi
	fi
	[ $SCRIPTING ] \
		&& [[ "$var" != '__LOCAL' ]] \
		&& echo "$var=${!var}"

	if [ -z ${!var} ] ; then
		return 1
	fi
}

#
#	xreadpass [msg] VAR
#
xreadpass(){
	local msg
	if [[ $# == 2 ]] ; then
		msg="$1 "
		shift
	fi
	if [[ ${!1} == 'SKIP' ]] ; then
		return
	fi
	local prefix=
	if [ $SCRIPTING ] ; then
		prefix='# '
	fi
	local PASS1
	local PASS2
	for attempt in 1 2 3 ; do
		read -sep "${prefix}${msg}password (Enter to skip): " PASS1
		echo
		if [ -z $PASS1 ] ; then
			return
		fi
		read -sep "${prefix}retype password: " PASS2
		echo
		if [[ $PASS1 != $PASS2 ]] ; then
			echo "ERROR: passwords do not match." >&2
			continue
		fi
		eval ''$1'='${PASS1}''
		return
	done
	return 1
}


# Review changes in PATH.new, then edit/apply changes to PATH
#
#	reviewApplyChanges PATH
#
# NOTE: if changes are not applied this will return non-zero making this 
#		usable in conditionals...
reviewApplyChanges(){
	local file=$1
	echo "# Review updated: ${file}.new:"
	@ cat ${file}.new
	echo '---'
	local res
	while true ; do
		read -ep "# [a]pply, [e]dit, [s]kip? " res
		case "${res,,}" in
			a|apply)
				break
				;;
			e|edit)
				${EDITOR} "${file}.new"
				;;
			s|skip)
				echo "# file saved as: ${file}.new"
				return 1
				;;
			*)
				echo "ERROR: unknown command: \"$res\"" >&2
				continue
				;;
		esac
	done
	@ mv -b "${file}"{.new,}
}



#----------------------------------------------------------------------

# 
# 	readConfig
#
# Envioronment variables:
#	CLEAN_RUN	- if set ignore ./config.last-run
#	CONFIG		- config file to load last
#
# XXX need this not to make this behave with explicitly set vars...
readConfig(){
	if [ -z $NO_DEFAULTS ] ; then
		local IFS=$'\n'
		#__ENV=($( (set -o posix ; set | grep -v 'BASHOPTS=') ))
		#__ENV=($( (declare -xp) ))
		[ -e ../config.global ] \
			&& source ../config.global
		[ -e ./config ] \
			&& source ./config
		# XXX is this the right priority for this???
		[ -e ./config.last-run ] \
			&& [ -z $CLEAN_RUN ] \
			&& source ./config.last-run
		[ -e "$CONFIG" ] \
			&& source $CONFIG
		#eval "${__ENV[@]}"
		#__ENV=
	fi
}


#
#	saveConfig [-d|-a] CONFIG VAR ..
#
saveConfig(){
	local prefix=
	local append=
	while true ; do
		case $1 in
			-d|--default)
				prefix=DFL_
				shift
				;;
			-a|--append)
				append=1
				shift
				;;
			*)
				break
				;;
		esac
	done
	local cfg=$1
	shift

	if [ -z $append ] ; then
		printf '' > "$cfg"
	fi
	{
		for var in $@ ; do
			echo "${prefix}${var}=${!var}"
		done
		echo
	} >> "$cfg"
}


saveLastRunConfig(){
	local cfg=config.last-run
	echo "# Saving config to: config.last-run"
	{
		echo "#"
		echo "# This file is auto-generated, any changes here will be overwritten." 
		echo "#"
	} > "$cfg"
	saveConfig -d -a "$cfg" ${XREAD_VARS[@]}
}


#
#	webAppConfig NAME
#
webAppConfig(){
	local name=${1^^}
	eval "${name}_SUBDOMAIN=\${${name}_SUBDOMAIN:=\${DFL_SUB${name}_DOMAIN}}
		${name}_SUBDOMAIN=\${${name}_SUBDOMAIN:+\${${name}_SUBDOMAIN%.}.}
		${name}_DOMAIN=\${${name}_DOMAIN:=\${DFL_${name}_DOMAIN}}
		# prioretize \${name}_*
		DFL_DOMAIN=\${DFL_DOMAIN:+\${${name}_SUBDOMAIN}\${DFL_DOMAIN}}
		DFL_DOMAIN=\${DOMAIN:+\${${name}_SUBDOMAIN}\${DOMAIN}}
		if [ \$${name}_DOMAIN ] ; then
			DFL_DOMAIN=\${${name}_SUBDOMAIN}\${${name}_DOMAIN}
		fi"
	# force check of domain...
	DOMAIN=
}


#
#	readVars
#	readCTHardwareVars
#	readBridgeVars
#
# Variables this handles:
#	EMAIL
#	DOMAIN
#	ID
#	CTHOSTNAME
#	WAN_BRIDGE
#	LAN_BRIDGE
#	ADMIN_BRIDGE
#	WAN_IP
#	WAN_GATE
#	LAN_IP
#	LAN_GATE
#	ADMIN_IP
#	ADMIN_GATE
#	ROOTPASS
#	PCT_EXTRA
#
# Variables this sets:
#	PASS
#
# Variables used:
# 	TMP_PASS_LEN
# 	ROOTPASS
#
readCTVars(){
	xread "ID: " ID
	xread "Hostname: " CTHOSTNAME

	# hardware...
	xread "CPU cores: " CORES
	xread "RAM (MB): " RAM
	xread "SWAP (MB): " SWAP
	xread "DRIVE (GB): " DRIVE
}
readBridgeVars(){
	# bridge config...
	xread "ADMIN bridge: vmbr" ADMIN_BRIDGE
	xread "WAN bridge: vmbr" WAN_BRIDGE
	xread "LAN bridge: vmbr" LAN_BRIDGE
}
readVars(){
	xread -n "Email: " EMAIL
	xread -n "Domain: " DOMAIN

	xread -n "Gate ID: " GATE_ID

	readCTVars

	readBridgeVars

	# gateway...
	# IPs can be:
	#	<empty>
	#	<IP>/<mask>
	#	dhcp
	# Gateways can be:
	#	<empty>
	#	<IP>
	# XXX these are the same...
	xread "WAN ip: " WAN_IP
	if [[ $WAN_IP != "dhcp" ]] ; then
		xread "WAN gateway: " WAN_GATE
	else
		WAN_GATE=
	fi
	xread "LAN ip: " LAN_IP
	if [[ $LAN_IP != "dhcp" ]] ; then
		xread "LAN gateway: " LAN_GATE
	else
		LAN_GATE=
	fi
	xread "ADMIN ip: " ADMIN_IP
	if [[ $ADMIN_IP != "dhcp" ]] ; then
		xread "ADMIN gateway: " ADMIN_GATE
	else
		ADMIN_GATE=
	fi

	# root password...
	if [ -z $ROOTPASS ] ; then
		xreadpass root PASS \
			|| exit 1
	else
		PASS=$ROOTPASS
	fi

	# extra stuff...
	xread "pct extra options: " PCT_EXTRA
}


#
#	makeTemplateSEDPatterns VAR ...
#
makeTemplateSEDPatterns(){
	local var
	for var in "$@" ; do
		local val=${!var}
		if [[ $val == SKIP ]] ; then
			val=
		fi
		echo "-e 's/\\\${${var}}/${val//\//\\/}/g'"
	done
}

# same as makeTemplateSEDPatterns but adds default vars + generates *_IPn vars...
PCT_TEMPLATE_VARS=(
	EMAIL
	DOMAIN
	CTHOSTNAME
	HOST_ADMIN_IP
	GATE_HOSTNAME
	GATE_LAN_IP
	GATE_ADMIN_IP
	NS_HOSTNAME
	NS_LAN_IP
	NS_ADMIN_IP
	WAN_IP
	WAN_GATE
	LAN_IP
	LAN_GATE
	ADMIN_IP
	ADMIN_GATE
)
makePCTTemplateSEDPatterns(){
	local vars=("${PCT_TEMPLATE_VARS[@]}" "$@")
	# strip ips and save to *_IPn var...
	local ip_vars=()
	local var
	local val
	for var in ${vars[@]} ; do
		if [[ $var =~ .*_IP ]] ; then
			local val=${!var}
			if [[ $val == SKIP ]] ; then
				val=
			fi
			ip_vars+=("${var}n")
			eval "local ${var}n=\"${val/\/*}\""
		fi
	done

	makeTemplateSEDPatterns "${vars[@]}" "${ip_vars[@]}"
}


#
#	expandTemplate PATH VAR ...
#	.. | expandTemplate VAR ...
#
PCT_TEMPLATE_PATTERNS=
expandTemplate(){
	if [ -t 0 ] ; then
		local input=$1
		shift
	else
		local input=/dev/stdin
	fi

	if [ -z "$PCT_TEMPLATE_PATTERNS" ] ; then
		local patterns=($(makeTemplateSEDPatterns "$@"))
	else
		local patterns=("${PCT_TEMPLATE_PATTERNS[@]}")
	fi

	cat "${input}" \
		| eval "sed ${patterns[@]}" 
}


#
#	expandPCTTemplate PATH [VAR ...]
#	.. | expandPCTTemplate [VAR ...]
#
expandPCTTemplate(){
	local input=
	if [ -t 0 ] ; then
		input=$1
		shift
	fi

	local PCT_TEMPLATE_PATTERNS=($(makePCTTemplateSEDPatterns "$@"))

	expandTemplate "${input}"
}


#
#	buildAssets [VAR ..]
#
# XXX add vars in filenames (???)
NOTES=NOTES.md
buildAssets(){
	local template_dir=${TEMPLATE_DIR:-templates}
	local assets_dir=${ASSETS_DIR:-assets}
	local staging_dir=${STAGING_DIR:-staging}

	local PCT_TEMPLATE_PATTERNS=($(makePCTTemplateSEDPatterns "$@"))

	# assets...
	if [ -e "${assets_dir}" ] ; then
		mkdir -p "${staging_dir}"
		cp -R "${assets_dir}"/* "${staging_dir}"/
	fi

	# template dir...
	if [ -e $template_dir ] ; then
		local TEMPLATES=($(find "$template_dir" -type f))
		for file in "${TEMPLATES[@]}" ; do
			file=${file#${template_dir}}
			echo Generating: ${file}...
			[ $DRY_RUN ] \
				&& continue
			# ensure the directory exists...
			mkdir -p "$(dirname "${staging_dir}/${file}")"
			cat "${template_dir}/${file}" \
				| expandTemplate \
				> "${staging_dir}/${file}"
		done
	fi

	# special case: NOTES.md...
	if [ -z "$DESCRIPTION" ] && [ -e "$NOTES" ] ; then
		DESCRIPTION="$(\
			cat ${NOTES} \
				| expandTemplate)"
	fi
}



#----------------------------------------------------------------------

#
#	pctPushAssets ID
#
pctPushAssets(){
	@ pct-push-r $1 "${STAGING_DIR:-./staging}" /
}


#
#	traefikPushConfig
#
# XXX generate config in a staging location...
TRAEFIK_CONFIG=traefik.yml
TRAEFIK_PATH=/etc/traefik.d/
TRAEFIK_STAGING=traefik/
traefikPushConfig(){
	local filename="${CTHOSTNAME}.yml"
	local source="${TRAEFIK_STAGING}/${filename}"
	local target="${TRAEFIK_PATH}"/"${filename}"

	# source file not found...
	if ! [ -e "${TRAEFIK_CONFIG}" ] ; then
		echo "${TRAEFIK_CONFIG}: not found." >&2
		return
	fi

	# generat config...
	mkdir -p "${TRAEFIK_STAGING}"
	cat ${TRAEFIK_CONFIG} \
		| expandPCTTemplate \
		> "${source}"

	# get things we need if they are not set...
	xread "Gate CT id: " GATE_ID

	# check if $filename exists...
	if @ lxc-attach $GATE_ID -- test -e ${target} \
			&& ! xreadYes "Overwrite existing \"${target}\"?" ; then
		@ lxc-attach $GATE_ID -- mv "${target}" "${target}.bak"
	fi

	@ pct push $GATE_ID "${source}" "${target}"
}


#
#	pveGetLatestTemplate PATTERN [VAR]
#
# see:
#	https://pve.proxmox.com/wiki/Linux_Container
pveGetLatestTemplate(){
	if [ $DRY_RUN ] ; then
		[ -z $2 ] \
			|| eval "$2=${CT_TEMPLATE:-\\\$CT_TEMPLATE}"
		return
	fi

	#@ pveam update 

	local templates=($(pveam available | grep -o ''${1}'.*$'))
	local latest=${templates[-1]}

	@ pveam download local ${latest}

	latest=$(pveam list local | grep -o "^.*$latest")
	#latest=($(ls /var/lib/vz/template/cache/${1}*))

	[ -z $2 ] \
		|| eval "$2=${latest}"
}


#
#	pctBaseCreate ID TEMPLATE ARGS [PASS]
#
pctBaseCreate(){
	local ID=$1
	local TEMPLATE=$2
	local ARGS=$3
	local PASS=$4

	local TMP_PASS=$(cat /dev/urandom | base64 | head -c ${TMP_PASS_LEN:=32})
	# NOTE: we are not setting the password here to avoid printing it to the terminal...
	@ pct create $ID \
		"${TEMPLATE}" \
		${ARGS} \
		--password="$TMP_PASS" \
		--start 1 \
	|| exit 1

	# set actual root password...
	if [ "$PASS" ] ; then
		echo "root:$PASS" \
			| @ lxc-attach $ID chpasswd
	fi
}

#
#	pctCreate ID TEMPLATE [PASS]
#
#OPTS_STAGE_1=
#INTERFACES=
#CTHOSTNAME=
#CORES=
#RAM=
#SWAP=
#DRIVE=
#PCT_EXTRA=
pctCreate(){
	# build network args...
	local interfaces_args=()
	local i=0
	local interface
	for interface in "${INTERFACES[@]}" ; do
		interfaces_args+=("--net${i} "${interface}"")
		i=$(( i + 1 ))
	done

	# NOTE: TKL gui will not function correctly without nesting enabled...
	local args="\
		--hostname $CTHOSTNAME \
		--cores $CORES \
		--memory $RAM \
		--swap $SWAP \
		"${interfaces_args[@]}" \
		--storage local-lvm \
		--rootfs local-lvm:$DRIVE \
		--unprivileged 1 \
		--features nesting=1 \
		${PCT_EXTRA} \
	"

	pctBaseCreate "$1" "$2" "${OPTS_STAGE_1:-"${args}"}" "$3"
}

#
#	pctCreate<distro> ID [PASS]
#
pctCreateAlpine(){
	local TEMPLATE
	pveGetLatestTemplate alpine TEMPLATE

	pctCreate $1 "$TEMPLATE" "$2"

	sleep ${TIMEOUT:=5}

	@ lxc-attach $1 apk update
	@ lxc-attach $1 apk upgrade
}
pctCreateDebian(){
	local TEMPLATE
	pveGetLatestTemplate 'debian-12-standard' TEMPLATE

	pctCreate $1 "$TEMPLATE" "$2"

	sleep ${TIMEOUT:=5}

	@ lxc-attach $1 apt update
	@ lxc-attach $1 -- apt upgrade -y
}
pctCreateUbuntu(){
	local TEMPLATE
	pveGetLatestTemplate ubuntu TEMPLATE

	pctCreate $1 "$TEMPLATE" "$2"

	sleep ${TIMEOUT:=5}

	@ lxc-attach $1 apt update
	@ lxc-attach $1 -- apt upgrade -y
}

#
#	pctCreateTurnkey APP ID [PASS]
#
pctCreateTurnkey(){
	local app=$1
	shift
	local TEMPLATE
	pveGetLatestTemplate '.*-turnkey-'$app TEMPLATE

	pctCreate $1 "$TEMPLATE" "$2"

	tklWaitForSetup $1

	sleep ${TIMEOUT:=5}
}

# Wait for /etc/inithooks.conf to be generated then cleared
#
# 	tklWaitForSetup ID
#
# for tkl inithooks doc see:
# 	https://www.turnkeylinux.org/docs/inithooks
tklWaitForSetup(){
	printf "# TKL setup, this may take a while"
	if [ -z $DRY_RUN ] ; then
		while ! $(lxc-attach $1 -- test -e /etc/inithooks.conf) ; do
			printf '.'
			sleep ${TIMEOUT:=5}
		done
		printf '+'
		sleep ${TIMEOUT:=5}
		while ! [[ $(lxc-attach $1 -- cat /etc/inithooks.conf | wc -c) < 2 ]] ; do
			printf '.'
			sleep ${TIMEOUT:=5}
		done
	else
		printf '.+..'
	fi
	printf 'ready.\n'
	sleep ${TIMEOUT:=5}
}

#
#	pctUpdateTurnkey ID
#
pctUpdateTurnkey(){
	@ lxc-attach $1 apt update
	@ lxc-attach $1 -- apt upgrade -y
}


#
#	pctSet ID [ARGS [REBOOT]]
#
pctSet(){
	[ "$2" ] \
		&& @ pct set $1 \
			${2}
	[ "$3" ] \
		&& @ pct reboot $1
}

#
#	pctSetNotes ID [DESCRIPTION]
#
pctSetNotes(){
	# XXX for some reason this complains quote alot...
	#[ "$DESCRIPTION" ] \
	#	&& @ pct set $1 \
	#		"${DESCRIPTION:+--description \""${DESCRIPTION}"\"}"
	local ID=$1
	local NOTES="$(\
		echo -e "${2:-${DESCRIPTION}}" \
			| sed -e 's/^/#/')"

	if [ "$DRY_RUN" ] ; then
		return
	fi

	local CONF="$(cat "${CT_DIR}/${ID}.conf")"
	local TEXT="\
"${NOTES}"
"${CONF}"
"
	echo -e "${TEXT}" > "${CT_DIR}/${ID}.conf"
}


#
#	showNotes [VAR ...]
#
BUILD_NOTES=BUILD_NOTES
showNotes(){
	[ -e "${BUILD_NOTES}" ] \
		&& mv "${BUILD_NOTES}"{,.bak}
	[ -e "${BUILD_NOTES}".tpl ] \
		&& ( cat "${BUILD_NOTES}".tpl \
			| expandPCTTemplate $@ \
			| tee "${BUILD_NOTES}" )
}

#
#	pushNotes ID
#
pushNotes(){
	[ -e "${BUILD_NOTES}" ] \
		&& @ pct-push-r $1 "${BUILD_NOTES}" /root/
}



#----------------------------------------------------------------------
# vim:set ts=4 sw=4 nowrap :
