#!/bin/bash
# Copyright (c) Dwarf Technologies s.r.o. 2024

fixname="dt_rmm_cmd"
sname="dwarfg"
lname="Dwarfguard"
call_prefix="${sname}_"
version="1.3"
cmdstrings=("help"
"status"
"stop"
"start"
"restart"
"reload"
"stats_proc"
"stats_app"
"logview"
"loggrep"
"logexport"
"backup"
"restore"
"grafint"
"deplist"
"depstatus"
"trafcnt")
cmdqhelp=(
"[command] ... list all commands with quickhelp or provide longer help on one command"
"... show runtime status of $lname service (${sname}d)"
"... stops $lname service"
"... starts $lname service"
"... restarts $lname service"
"... reloads dwarflib configuration"
"... shows systemd-gathered information (if under systemd)"
"... shows app stats like # of devices, license, last restart"
"... opens daemon log in ${EDITOR:-view}"
"[level] ... displays logs messages. CRIT|ERR|*WARN*|NOTE"
"[fname] ... create log archive. may add suffix: fname[.tgz]"
"... run backup NOW (downtime!) via ${sname}_upgrade.sh"
"... interactive mode: ${sname}_upgrade.sh (backup+restore)"
"... enable grafana integration and push (re-push) dashboard"
"... show all $lname deployments found on server"
"... show service status for each deployment"
"... count traffic for device/monitoring group over period of time. Call with -h for help.")
c_status=1
c_stop=2
c_start=3
c_restart=4
c_reload=5
c_stats_proc=6
c_stats_app=7
c_logview=8
c_loggrep=9
c_logexport=10
c_backup=11
c_restore=12
c_grafint=13
c_deplist=14
c_depstatus=15
c_trafcnt=16
declare -a depnums
declare -a depnames
declare -a depnotes
curl_json_out=
curl_http_res=0
gr_table_accesslist="grafana_table_accesslist.txt"
gr_datasource_uid="grafana_datasource_uid.txt"
gr_dashboard_uid="grafana_dashboard_uid.txt"
gr_dashboard_template="grafana_dashboard_template.json"
gr_tmpdashb="/tmp/dwarfg_grafana_dashboard.txt"
gr_tmpapaconf="/tmp/dwarfg_tmpapaconf"

list_commands() {
	mute=""
	[ $# -gt 0 ] && mute="."
	[ -z "$mute" ] && echo -e "Supported commands: (call \"${sname}_command\" or \"$fixname command\")\n"
	for i in "${!cmdstrings[@]}"; do
		if [ -n "$mute" ] ; then
			echo "${cmdstrings[$i]}"
		else
			echo -e "${cmdstrings[$i]} ${cmdqhelp[$i]}"
		fi
	done
	echo
}

help() {
	[[ $# -eq 1 && "$1" = "mute" ]] && { list_commands "."; return 0; }
	echo -e "\n$lname command-line interface version $version"
	local pat="$1"
	if [ "$1" = "help" ] ; then
		pat="$2"
	fi
	case "$pat" in
		"${cmdstrings[$c_stop]}"|"${cmdstrings[$c_start]}"|"${cmdstrings[$c_restart]}")
			echo -e "\nstop|start|restart ... controls app runtime status via ${sname}_ctl.sh"
			echo -e "\t$lname may be integrated via systemd. CTL script uses systemd for actions then."
			;;
		"${cmdstrings[$c_status]}"|"${cmdstrings[$c_stats_proc]}")
			echo -e "\nstatus ... displays process status using ${sname}_ctl.sh script"
			echo -e "stats_proc ... displays systemd-gathered information (if integrated in systemd)"
			;;
		"${cmdstrings[$c_reload]}")
			echo -e "\nreload ... reloads dwarflib configuration (usually dwarflib_cfg.txt)"
			;;
		"${cmdstrings[$c_stats_app]}")
			echo -e "\nstats_app ... display app status - # of devices, license state, last restart etc."
			;;
		"${cmdstrings[$c_logview]}"|"${cmdstrings[$c_loggrep]}"|"${cmdstrings[$c_logexport]}")
			echo -e "\nlogview ... opens app log in ${EDITOR:-view}"
			echo -e "loggrep [<loglevel>] ... shows <loglevel> and more serious logs. Note that if you have log throttling enabled (standard on production) and troubleshooting, you want to disable it first."
			echo -e "\t<loglevel> can be any of CRIT|ERR|WARN|NOTE|INFO|DEBUG. Default is WARN."
			echo -e "logexport [filename] ... calls grab_logs.sh script to produce log archive"
			echo -e "\tIf filename given, archive name is filename[.tgz] - suffix added if missing"
			;;
		"${cmdstrings[$c_backup]}"|"${cmdstrings[$c_restore]}")
			echo -e "\nbackup ... performs backup NOW (incurs downtime!) via ${sname}_upgrade.sh"
			echo -e "restore ... calls interactive mode of ${sname}_upgrade.sh (backup+restore)"
			;;
		"${cmdstrings[$c_grafint]}")
			echo -e "\ngrafint ... trigger grafana installation and configuration (if not installed)"
			echo -e "        ... it also does push the default dashboard into grafana"
			echo -e "        ... usable to re-push the dashboard if user messes the dashboard up"
			;;
		"${cmdstrings[$c_deplist]}")
			echo -e "\ndeplist ... shows all ${sname} deployments found on server"
			;;
		"${cmdstrings[$c_depstatus]}")
			echo -e "\ndepstatus ... shows status of each of the ${sname} deployements on server"
			;;
		"${cmdstrings[$c_trafcnt]}")
			print_traffic_help
			;;
		*)
			echo -e "\nsyntax1: $fixname <command> [command options...] [--dn <number>] [--dd <string>]"
			echo -e "syntax2: ${call_prefix}<command> [command options...] [--dn <number>] [--dd <string>]"
			echo -e "\tcommand options are noted below in the command list"
			echo -e "\t--dn number ... select deployment by its deployment (not order!) number"
			echo -e "\t--dd string ... select deployment by its deployment name\n"
			list_commands
			;;
	esac
}

f_cmdlist() {
	list_commands ""
}

f_status() {
	echo -n "$1: "
	"/opt/$1/${sname}_ctl.sh" --status
}

f_stop() {
	f_status "$1"
	"/opt/$1/${sname}_ctl.sh" --stop
	[ -z "$2" ] && f_status "$1"
}

f_start() {
	[ -z "$2" ] && f_status "$1"
	"/opt/$1/${sname}_ctl.sh" --start
	f_status "$1"
}

f_restart() {
	f_stop "$1" "restart"
	f_start "$1" "restart"
}

f_reload() {
	"/opt/$1/${sname}_ctl.sh" --reload-cfg
}

f_stats_proc() {
	systemctl status "$depname"
}

f_stats_app() {
	lictxt=""
	dbname="$1"
	dbname="${dbname//[.-]/_}"
	devices=$(mysql "$dbname" -Nse "SELECT COUNT(devid) FROM devices")
	prodname="$(grep "^NAME" "/opt/$1/base_defs" | sed "s/.*=\"\\([^\"]*\\)\"/\\1/")"
	suplink="$(mysql "$dbname" -Nse "SELECT strval from product_conf where name='SupportLink'")"
	myoname="$(pwd)"
	cd "/opt/$1" || return 1
	if out=$("./licman" -c) ; then
		lictxt="License is valid. Use \"cd /opt/$1 && ./licman -d\" to see license dump."
	else
		lictxt="ERROR: licensing problem. Use \"cd /opt/$1 && ./licman -c\" to know more."
	fi
	cd "$myoname" || return 1
	"/opt/$1/${sname}d" -v || {
		echo -e "\nERROR: \"/opt/$1/${sname}d\" -v returned error. Your installation is CORRUPTED. Restore from backup.\n" >&2
		exit 1
	}
	echo -n "$prodname status via CTL script: "
	"/opt/$1/${sname}_ctl.sh" --status
	dpid=$("/opt/$1/${sname}_ctl.sh" --pid)
	seats=$(echo "$out" | grep "# of device seats" | sed "s/.*: //")
	echo -n "$prodname is licensed for use by: "
	echo "$out" | grep "^Licensee:" | sed "s/.*: //"
	echo "$lictxt"
	echo "Devices registered/seats licensed: $devices/$seats"
	echo -n "License valid till "
	echo "$out" | grep "Valid till" | sed "s/.*: //"
	echo "Your support link (empty if self-support): $suplink"
	echo "Basic product variables values:"
	grep -E "^VERSION|^DOMAIN|^EXTERNURL|^SERVID|^SERV_TUNSSH_PORT|^USE_SSL|^DWARFG_PORT|^LISTENER_THREADS" "/opt/$1/base_defs" | sed "s/^/\t/"
	echo "Overrides defined in /opt/$1/${sname}.ini:"
	grep -E "^SERV_TUNSSH_PORT|^LISTENER_THREADS" "/opt/$1/${sname}.ini" | sed "s/^/\t/"
	[ -n "$dpid" ] && {
		echo "Daemon threads breakdown:"
		ps -p "$dpid" -T -o pid,rss,comm,cmd,pcpu,stime,etimes,times
	}
	echo "Service status via systemd:"
	sstat=$(systemctl status "$1" 2>&1)
	echo "$sstat"
}

f_logview() {
	editor=${EDITOR:-view}
	"$editor" "/srv/$1/logs/log_$sname.txt"
}

f_loggrep() {
	case "$2" in
		"CRIT")
			pat="^[^/]*/CRIT|INIT"
			;;
		"ERR")
			pat="^[^/]*/ERR|^[^/]*/CRIT|INIT"
			;;
		"NOTE")
			pat="^[^/]*/NOTE|^[^/]*/WARN|^[^/]*/ERR|^[^/]*/CRIT|INIT"
			;;
		*)
			pat="^[^/]*/WARN|^[^/]*/ERR|^[^/]*/CRIT|INIT"
			;;
	esac
	logf="/srv/$1/logs/log_${sname}.txt"
	[ ! -s "$logf" ] && echo "Logfile \"$logf\" not found or empty!" >&2 && return 1
	grep -E "$pat" "$logf"
}

f_logexport() {
	file="$("/opt/$1/grab_logs.sh" | tail -1)"
	if [[ -z "$file" || ! -s "$file" ]] ; then
		echo "Failed to produce logs. Try running /opt/$1/grab_logs.sh manually." >&2
	else
		if [ -n "$2" ] ; then
			if echo "$2" | grep -q "\.tgz$" ; then
				target_file="$2"
			else
				target_file="$2.tgz"
			fi
			cp "$file" "$target_file"
			echo "Copied $file to $target_file"
		else
			echo "Output filepath is: $file"
		fi
	fi
}

f_backup() {
	"/opt/$1/${sname}_upgrade.sh" backup
}

f_restore() {
	"/opt/$1/${sname}_upgrade.sh"
}

f_deplist() {
	local ctr=0
	if [ ${#depnums[@]} -eq 0 ] ; then
		echo "No $lname deployments found!" >&2
	else
		echo "Found ${#depnums[@]} deployments. Order and dep numbers and domains:"
		for i in "${!depnums[@]}" ; do
			ctr=$((ctr+1))
			echo -en "$ctr. \t${depnums[$i]}: ${depnames[$i]}"
			if [ -n "${depnotes[$i]}" ] ; then
				echo -n "... ${depnotes[$i]}"
			fi
			if [[ $# -eq 1 && "$1" = "showstatus" ]] ; then
				echo -en "\n\t\tStatus: "
				"/opt/${depnames[$i]}/${sname}_ctl.sh" --status
			else
				echo
			fi
		done
	fi
}

f_depstatus() {
	f_deplist "showstatus"
}

f_json_patmatch() {
	[ $# -eq 2 ] || return 1
	pattern="$1"
	json="$2"
	echo "$json" | tr ',' '\n' | grep "\"$pattern\":" | head -1 | sed "s/.*\"$pattern\"[ ]*:[ ]*\"\([^\"]*\).*/\1/"
}

f_grafapi() {
	[ $# -ne 5 ] && {
		echo "grafana api call and parse function - need exactly 5 parameters" >&2
		return 1
	}
	request="$1"
	json_in="$2"
	pattern="$3"
	curladd="$4"
	http_ok="$5"
	[ -z "$http_ok" ] && http_ok=0
	curl_json_out=
	api_url="http://admin:$GRADMPASS@localhost:3000/$request"
	semi_fail=""
	if [ -n "$json_in" ] ; then
		if (echo "$json_in" | grep "^file:" >/dev/null) ; then
			realfile="$(echo "$json_in" | sed "s/^file://")"
			[ -f "$realfile" ] || {
				echo "JSON file to post ($realfile) is missing."
				return 2
			}
			my_out=$(curl --no-progress-meter --json "@$realfile" -w "\nHTTP_CODE=%{http_code}\n" $curladd "$api_url") || {
				echo "Grafana API call failed to connect to Grafana (API URL: \"$api_url\")" >&2
				return 3
			}
		else
			my_out=$(echo -e "$json_in" | curl --no-progress-meter --json @- -w "\nHTTP_CODE=%{http_code}\n" $curladd "$api_url") || {
				echo "Grafana API call failed to connect to Grafana (API URL: \"$api_url\")" >&2
				return 4
			}
		fi
	else
		my_out=$(curl --no-progress-meter $curladd -w "\nHTTP_CODE=%{http_code}\n" "$api_url") || {
			echo "Grafana API call failed to connect to Grafana (API URL: \"$api_url\")" >&2
			return 5
		}
	fi
	curl_http_res=$(echo -e "$my_out" | tail -1 | sed "s/.*HTTP_CODE=\([0-9]*\)$/\1/")
	if [[ -z "$curl_http_res" || "200" != "$curl_http_res" ]] ; then
		[ "$curl_http_res" = "$http_ok" ] || {
			echo -e "Grafana API call failed with \"$curl_http_res\"" >&2
			json_err_message=$(f_json_patmatch "message" "$my_out") && {
				if [ -n "$json_err_message" ] ; then
					echo -e "Error message was: \"$json_err_message\""
				fi
			}
			return 6
		}
		semi_fail="yes"
	fi
	curl_json_out=$(echo "$my_out" | sed \$d)
	curl_json_res=
	if [[ -z "$semi_fail" && -n "$pattern" ]] ; then
		curl_json_res=$(f_json_patmatch "$pattern" "$curl_json_out") || echo "Error when matching pattern in returned json." >&2
		[ -n "$curl_json_res" ] || {
			echo "Grafana API call failed to return expected pattern." >&2
			return 7
		}
	fi
	return 0
}

f_getuid_from_file() {
	[[ $# -eq 1 && -n "$1" ]] || return 1
	[ -f "$1" ] && {
		candy=$(cat "$1")
		if [[ -n "$candy" && "$candy" = "${candy//[^0-9a-z]}" ]] ; then
			echo "$candy"
		else
			echo 0
			echo "NOTE: no or garbled number in $1" >&2
		fi
	}
	return
}

f_grafint() {
	DIR="/opt/$1"
	grconf="/etc/grafana/grafana.ini"
	dbname="$1"
	gruser="dwarf"
	GRDBPASS=${GRDBPASS:-}
	GRADMPASS=${GRADMPASS:-}
	. "$DIR"/base_defs || return 1
	. "$DIR"/deploy_funcs.sh || return 1
	read_defs || return 1
	"$DIR"/machine_install.sh grafana || return 1
	dbname="${dbname//[.-]/_}"
	mydomain="$DOMAIN"
	[ -z "$mydomain" ] && {
		echo "Domain is empty, using localhost domain for Grafana... "
		mydomain="localhost"
	}
	echo "Waiting for grafana to finish background setup... (30 seconds)" && sleep 30
	if [ -z "$GRADMPASS" ] ; then
		new_gradmpass || return 1
		read_defs || return 1
		echo "Setting grafana admin password to \"$GRADMPASS\""
		grafana-cli admin reset-admin-password "$GRADMPASS" || return 1
	else
		echo "Grafana admin password left at \"$GRADMPASS\""
	fi
	if [ -z "$GRDBPASS" ] ; then
		new_grdbpass || return 1
		read_defs || return 1
		echo "Setting grafana DB user password to \"$GRDBPASS\""
	else
		echo "Grafana DB user password left at \"$GRDBPASS\""
	fi
	[[ -z "$GRDBPASS" || -z "$GRADMPASS" ]] && return 1
	echo "Adding grafana user $gruser with password dgpwd ..."
	f_grafapi "api/admin/users" "{ \"name\":\"$gruser\", \"login\":\"$gruser\", \"password\": \"dgpwd\" }" "" "" 412
	[ "$curl_http_res" = "412" ] && echo " ... user exists already."
	sed -i "s#^DWARFG_UI_GRAFANA_USER=.*#DWARFG_UI_GRAFANA_USER='$gruser'#" "$GUIDIR/.env" || {
		echo "Failed to replace Grafana user in GUI config file ($GUIDIR/.env)" >&2
		return 1
	}
	sed -i "s#^DWARFG_UI_GRAFANA_PASS=.*#DWARFG_UI_GRAFANA_PASS='dgpwd'#" "$GUIDIR/.env" || {
		echo "Failed to replace Grafana password in GUI config file ($GUIDIR/.env)" >&2
		return 1
	}
	echo "Adding DB user for Grafana ..."
	mysql "$dbname" <<EOF || {
CREATE USER IF NOT EXISTS 'dg_grafana'@'localhost' IDENTIFIED BY '$GRDBPASS'
EOF
		echo "Unable to create dg_grafana DB user" >&2
		return 1
	}
	[ -f "$DIR/$gr_table_accesslist" ] || {
		echo "Deployment is missing grafana table list." >&2
		return 1
	}
	echo "Allowing Grafana DB user to access selected tables ..."
	for table in $(<"$DIR"/grafana_table_accesslist.txt); do
		echo "GRANT SELECT ON $dbname.${table} TO 'dg_grafana'@'localhost';"
	done | mysql "$dbname"
	dsuid=$(f_getuid_from_file "$DIR/$gr_datasource_uid")
	[ -n "$dsuid" ] && echo "Using grafana datasource UID: $dsuid. If operation fails, you can delete $DIR/$gr_datasource_uid and retry - new datasource will be defined."
	[ -z "$dsuid" ] && {
		echo "Creating new datasource ..."
		# define new datasource, get its uid and store into file
		curl_json_res=
		f_grafapi "api/datasources" "{ \"name\":\"$dbname\", \"access\":\"proxy\", \"type\":\"mysql\", \"host\":\"localhost:3306\", \"database\":\"$dbname\", \"user\":\"dg_grafana\", \"secureJsonData\": { \"password\": \"$GRDBPASS\" } }" "uid" "" 409
		[ "$curl_http_res" = "409" ] && {
			# datasource exists already (with the same name) - get the uid by datasource name
			echo "Datasource exists already, detecting its uid..."
			curl_json_res=""
			f_grafapi "api/datasources/name/$dbname" "" "uid" "" ""
		}
		[ -z "$curl_json_res" ] && {
			echo "Failed to get datasource uid (is empty)" >&2
			return 1
		}
		[ "$curl_json_res" != "${curl_json_res//[^0-9a-z]}" ] && {
			echo "Invalid datasource UID returned from Grafana ($curl_json_res)"
			return 1
		}
		echo "Saving new datasource into $DIR/$gr_datasource_uid ..."
		dsuid=$curl_json_res
		echo "$dsuid" >"$DIR/$gr_datasource_uid"
	}
	dbuid=$(f_getuid_from_file "$DIR/$gr_dashboard_uid")
	[ -n "$dbuid" ] && {
		echo "Deleting grafana dashboard UID: $dbuid ..."
		f_grafapi "api/dashboards/uid/$dbuid" "" "" "-X DELETE" ""
	}
	sed "s/DATASOURCEUID/$dsuid/g" "$DIR/$gr_dashboard_template" >"$gr_tmpdashb" || {
		echo "Unable to write to $gr_tmpdashb"
		return 1
	}
	echo "Pushing dashboard into Grafana ..."
	curl_json_res=
	f_grafapi "api/dashboards/db" "file:$gr_tmpdashb" "uid" "" 412 || {
		echo "Unable to add new Grafana dashboard ($?), integration failed."
		return 1
	}
	[ "$curl_http_res" = "412" ] && {
		echo "Conflicting dashboard detected. Attempting to detect uid and remove."
		f_grafapi "api/search?type=dash-db" "" "" "" "" || {
			echo "Failed to get dashboard list from Grafana. Remove the 'Traffic counter' dashboard manually and retry the integration."
			return 1
		}
		target_uid=""
		readarray -t dashbs_array < <(jq -c '.[]' <<< "$curl_json_out")
		for item in "${dashbs_array[@]}" ; do
			json_type=$(jq --raw-output '.type' <<< "$item")
			json_title=$(jq --raw-output '.title' <<< "$item")
			json_uid=$(jq --raw-output '.uid' <<< "$item")
			[[ "$json_type" = "dash-db" && "$json_title" = "Traffic counter" ]] && {
				target_uid=$json_uid
			}
		done
		[ -z "$target_uid" ] && {
			echo "Unable to detect dashboard uid. Please remove the 'Traffic counter' dashboard manually and retry the integration."
			return 1
		}
		f_grafapi "api/dashboards/uid/$target_uid" "" "" "-X DELETE" "" || {
			echo "Unable to remove 'Traffic dashboard' with uid '$target_uid'. Please remove manually and retry integration."
			return 1
		}
		f_grafapi "api/dashboards/db" "file:$gr_tmpdashb" "uid" "" "" || {
			echo "Unable to add new Grafana dashboard ($?), integration failed."
			return 1
		}
	}
	[ -n "$curl_json_res" ] || {
		echo "Failed to get dashboard uid (is empty). Integration failed." >&2
		return 1
	}
	[ "$curl_json_res" != "${curl_json_res//[^-0-9a-z]}" ] && {
		echo "Invalid dashboard UID returned from Grafana ($curl_json_res). Integration failed."
		return 1
	}
	dbuid=$curl_json_res
	echo "Saving new dashboard UID ($curl_json_res) into $DIR/$gr_dashboard_uid ..."
	echo "$dbuid" >"$DIR/$gr_dashboard_uid"
	randlink="$(get_randpass 10)"
	[[ -z "$randlink" || ! -f "$BINDIR/$DWARFG_APACONF" ]] && {
		echo "Unable to configure Apache proxy to grafana for $dbname" >&2
		return 1
	}
	echo "Updating Apache2 webserver config so that it serves Grafana under a sub-URL ..."
	# Apache config:
	# 0. copy the config to temporary file
	# 1. remove old grafana proxy lines if there
	cat "$BINDIR/$DWARFG_APACONF" | grep -v "^[^#]*ProxyPassMatch.*grafana" | grep -v "^[^#]*ProxyPassReverse.*grafana"  >"$gr_tmpapaconf"
	# 2. grab the relevant template lines (2)
	line_prox="$(grep "#ProxyPassMatch.*grafana" $gr_tmpapaconf | head -1)"
	line_rev="$(grep "#ProxyPassReverse.*grafana" $gr_tmpapaconf | head -1)"
	[[ -z "$line_prox" || -z "$line_rev" ]] && {
		echo "Apache config file misses template grafana lines as comments, unable to generate Apache grafana link." >&2
		rm "$gr_tmpapaconf"
		return 1
	}
	line_prox="$(echo "$line_prox" | sed "s/#ProxyPassMatch/ProxyPassMatch/" | sed "s/GHASH/$randlink/" | sed "s/DASHBOARD_UID/$dbuid/")"
	line_rev="$(echo "$line_rev" | sed "s/#ProxyPassReverse/ProxyPassReverse/" | sed "s/GHASH/$randlink/" | sed "s/DASHBOARD_UID/$dbuid/")"
	# 3. add the substituded lines before the template lines
	sed -i "\;#ProxyPassMatch.*grafana;i $line_prox" "$gr_tmpapaconf"
	sed -i "\;#ProxyPassReverse.*grafana;i $line_rev" "$gr_tmpapaconf"
	# 4. move the resulting config over the original one
	mv "$gr_tmpapaconf" "$BINDIR/$DWARFG_APACONF" || {
		echo "Failed to move the new Apache2 config in place ($gr_tmpapaconf -> $BINDIR/$DWARFG_APACONF)" >&2
		return 1
	}
	echo "Updating GUI configuration file with Grafana URL ..."
	# Env file:
	# replace the variable in place
	sed -i "s#^DWARFG_UI_GRAFANA_REDIR=.*#DWARFG_UI_GRAFANA_REDIR='grafana_$randlink/d/$dbuid'#" "$GUIDIR/.env" || {
		echo "Failed to replace Grafana URL in GUI config file ($GUIDIR/.env)" >&2
		return 1
	}
	echo "Updating Grafana server configuration with currect URL ..."
	# Grafana configuration
	[ ! -f "$grconf" ] && {
		echo "Cannot locate Grafana configuration file ($grconf)." >&2
		return 1
	}
	target_cfgline="root_url = https://$mydomain/grafana_$randlink/"
	if grep "^root_url *=" $grconf >/dev/null; then
		sed -i "0,/^root_url *=/s#^root_url *=.*#${target_cfgline}#" "$grconf" || {
			echo "Failed to update Grafana configuration file ($grconf)." >&2
			return 1
		}
	else
		if grep "^;root_url *=" $grconf >/dev/null; then
			sed -i "0,/^;root_url *=/s#^;root_url *=.*#${target_cfgline}#" "$grconf" || {
				echo "Failed to update Grafana configuration file [2] ($grconf)." >&2
				return 1
			}
		else
			echo "Failed to locate proper place in grafana config file to put the root_url directive into." >&2
			return 1
		fi
	fi
	echo "Restarting Grafana and Apache2 for the changes to take effect ..."
	systemctl restart grafana-server
	systemctl reload apache2
}

count_traffic() {
	[ $# -eq 8 ] || return 1
	local dbname="$1"
	local groupid="$2"
	local devid="$3"
	local traftype="$4"
	local trafperiod="$5"
	local time_start="$6"
	local time_end="$7"
	local csv="$8"
	local mysqlopt=""
	if [ -n "$groupid" ] ; then
		[ "$groupid" != "${groupid//[^0-9]}" ] && {
			echo "Monitoring group ID ('$groupid') is not a number." >&2
			return 1
		}
		selection="n.id_device IN (SELECT id_device FROM monitoring_group_device_map WHERE mongrp_id = $groupid)"
	else
		[[ -z "$devid" || "$devid" != "${devid//[^0-9]}" || 0 -gt $devid || 65535 -lt $devid ]] && {
			echo "Device ID is either empty, or ivalid (is '$devid', must be number between 0 and 65535)." >&2
			return 1
		}
		selection="n.id_device = $devid"
	fi
	[[ "$traftype" != "${traftype//[^0-9]}" || $traftype -gt 2 || $traftype -lt 0 ]] && {
		echo "Unsupported traffic type - must be between 0 and 2 (inclusive, is: '$traftype', '${traftype//[^0-9]}')" >&2
		return 1
	}
	[[ "$trafperiod" != "${trafperiod//[^0-9]}" || $trafperiod -gt 2 || $trafperiod -lt 0 ]] && {
		echo "Unsupported traffic period - must be between 0 and 2 (inclusive, is: '$trafperiod')" >&2
		return 1
	}
	[ -n "$csv" ] && mysqlopt="--batch --raw"
	sql_query_base_1="sum(t.rx_bytes) AS Received, sum(t.tx_bytes) AS Transmitted FROM device_netdevs n JOIN"
	sql_query_base_2="t ON n.id = t.netdev where t.time >= '$time_start' AND t.time <= '$time_end' AND $selection"
	# following based on trfaperiod - daily, monthly, totals
	sql_head=("SELECT t.time AS Day," "SELECT t.time AS Month," "SELECT")
	sql_table=("netdevs_daily_traffic" "netdevs_monthly_traffic" "netdevs_daily_traffic")
	sql_groupby=("GROUP BY t.time" "GROUP BY t.time" "")
	# now based on traftype - all, cellular, non-cellular
	sql_traftype=("" "AND n.is_cellular = 1" "AND n.is_cellular = 0")
	sql_preface=("All traffic" "Cellullar Traffic" "Non-cellullar traffic")
	sql="${sql_head[$trafperiod]} $sql_query_base_1 ${sql_table[$trafperiod]} $sql_query_base_2 ${sql_traftype[$traftype]} ${sql_groupby[$trafperiod]}"
	echo "${sql_preface[$traftype]}"
	if [ -n "$csv" ] ; then
		mysql "$dbname" $mysqlopt -e "$sql" | tr '\t' ','
	else
		mysql "$dbname" $mysqlopt -e "$sql"
	fi
	return 0
}

print_traffic_help() {
	echo "$0 syntax and options:"
	echo "   $0 [-a]|-c|-n -g|-d <name> [-D]|-M|-T -s <date> -e <date> [-C]"
	echo "       -a        ... count All traffic types together (default)"
	echo "       -c        ... count only Cellular traffic"
	echo "       -n        ... count only Non-cellular traffic"
	echo "       -g <name> ... count traffic for monitoring Group. (-g \"All devices\")"
	echo "       -d <name> ... count traffic for Device. (-d ABAB)"
	echo "       -D        ... display Daily counters (default)"
	echo "       -M        ... display Monthly totals over a time period"
	echo "       -T        ... display Total traffic for a time period"
	echo "       -s <date> ... specify start date YYYY-MM-DD (-s 2024-01-01)"
	echo "       -e <date> ... specify end date YYYY-MM-DD (-s 2024-02-01)"
	echo "       -C        ... produce CSV file. You may want to redirect to a file. (>filename.csv)"
	echo
	echo "   Examples:"
	echo "       $0 -g \"All devices\" -s 2024-10-01 -e 2024-10-31"
	echo "                 ... Show daily totals for all devices during October"
	echo "       $0 -c -d ABAB -M -s 2024-01-01 -e 2024-12-31 -C"
	echo "                 ... Show CSV with monthly cellular traffic over year 2024 for device ABAB"
	echo
}

check_date() {
	[ $# -eq 1 ] || return 1
	[[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || return 1
	date "+%F" -d "$1" >/dev/null 2>&1 || return 1
}

f_trafcnt() {
	local devid
	local groupid
	local csv
	local traftype=0 # 0 ... all traffic, 1 ... cellular, 2 ... non-cellular
	local trafperiod=0 # 0 ... daily, 1 ... monthly, 2 ... totals
	dbname="$1"
	dbname="${dbname//[.-]/_}"
	shift
	[[ $# -lt 1 || "$1" = "-h" || "$1" = "--help" || $# -lt 2 ]] && {
		print_traffic_help
		[[ "$1" = "-h" || "$1" = "--help" ]] && return 0
		return 1
	}
	while [ $# -gt 0 ] ; do
		case $1 in
			-a)
				traftype=0
				shift
				;;
			-c)
				traftype=1
				shift
				;;
			-n)
				traftype=2
				shift
				;;
			-g)
				gid="$2"
				shift 2
				;;
			-d)
				devid="$2"
				shift 2
				;;
			-T)
				trafperiod=2
				shift
				;;
			-M)
				trafperiod=1
				shift
				;;
			-D)
				trafperiod=0
				shift
				;;
			-C)
				csv="yes"
				shift
				;;
			-s)
				date_start="$2"
				shift 2
				;;
			-e)
				date_end="$2"
				shift 2
				;;
			*)
				echo "Unknown argument ($1)"
				return 1
		esac
	done
	# check either gid or devid is set
	if [[ -z "$gid" && -z "$devid" ]] || [[ -n "$gid" && -n "$devid" ]] ; then
		echo "Exactly one of monitoring group name (-g \"name\") or device name (-d \"name\") must be provided." >&2
		return 1
	fi
	# check both start and end dates are defined and valid
	[[ -z "$date_start" || -z "$date_end" ]] && {
		echo "You must provide both start date (-s) and end date (-e)"
		return 1
	}
	check_date "$date_start" || {
		echo "Invalid start date provided. Use YYYY-MM-DD format." >&2
		return 1
	}
	check_date "$date_end" || {
		echo "Invalid end date provided. Use YYYY-MM-DD format." >&2
		return 1
	}
	numid=
	# check if monitoring group / device exists and get its numeric id
	[ -n "$gid" ] && numid=$(mysql "$dbname" -Nse "SELECT id FROM monitoring_groups WHERE name = '$gid'");
	[ -n "$devid" ] && numid=$(mysql "$dbname" -Nse "SELECT id_device FROM devices WHERE devid = '$devid'");
	[[ -z "$numid" || "$numid" != "${numid//[^0-9]}" ]] && {
		echo "Unable to fetch nemuric ID for group or device name. Mis-typed group or device name?" >&2
		return 1
	}
	numeric_gid=
	numeric_devid=
	if [ -n "$gid" ] ; then
		numeric_gid=$numid
	else
		numeric_devid=$numid
	fi
	count_traffic "$dbname" "$numeric_gid" "$numeric_devid" "$traftype" "$trafperiod" "$date_start" "$date_end" "$csv"
}


func=""
[[ $# -eq 1 && "$1" = "lscmd" ]] && { help mute; exit 0; }
# first try parsing the function name from $0...
cmdmatch=$(basename "$0")
if [ "$cmdmatch" = "$call_prefix${cmdstrings[0]}" ] ; then
	# if help requested, call immediately with no additional parsing
	help "$@"
	exit 0
fi
for i in $(seq 1 $((${#cmdstrings[@]}-1))); do
	if [ "$cmdmatch" = "$call_prefix${cmdstrings[$i]}" ] ; then
		if [[ $# -eq 1 && "$1" = "help" ]] ; then
			# <anything> help ==> help <anything>
			help "${cmdstrings[$i]}"
			exit 0
		fi
		func="${cmdstrings[$i]}"
		break
	fi
done
# if not found, then try getting it from $1
[[ -z "$func" && $# -ge 1 ]] && for i in $(seq 1 $((${#cmdstrings[@]}-1))); do
	if [ "$1" = "${cmdstrings[$i]}" ] ; then
		shift # throw out the first parameter as that was the function requested
		func="${cmdstrings[$i]}"
		break
	fi
done
if [ -n "$func" ] ; then
	depname=
	declare -a cmdpars
	shopt -s nullglob
	for i in "/opt/cache_$sname/${sname}_deployments/"* ; do
		j=$(basename "$i")
		num="${j%%_*}"
		name="${j#*_}"
		addon=""
		[ ! -d "/opt/$name" ] && addon="ERROR: failed to find the deployment directory (/opt/$name)!"
		depnums+=("$num")
		depnames+=("$name")
		depnotes+=("$addon")
	done
	if [ ${#depnums[@]} -eq 0 ] ; then
		echo "No $lname deployments found, bailing out!" >&2
		exit 1
	elif [ ${#depnums[@]} -eq 1 ] ; then
		depname="${depnames[0]}"
	elif [[ "$func" != "deplist" && "$func" != "depstatus" && "$func" != "help" ]] ; then
		# iterate over rest of parameters, see if there is selection by num or name
		pardet=""
		byname=""
		token=""
		for arg in "$@" ; do
			if [ -z "$pardet" ] ; then
				if [ "$arg" = "--dn" ] ; then
					pardet="yes"
				elif [ "$arg" = "--dd" ] ; then
					pardet="yes"
					byname="yes"
				else
					cmdpars+=("$arg")
				fi
			elif [ -z "$token" ] ; then
				token="$arg"
				[[ -n "$byname" && "${token:0:${#call_prefix}}" != "$call_prefix" ]] && {
					token="$call_prefix$token"
					echo -e "\tNOTE: switching to deployment name $token ..."
				}
			else
				cmdpars+=("$arg")
			fi
		done
		# check if we can select based on parameters
		if [ -n "$pardet" ] ; then
			if [ -n "$byname" ] ; then
				for i in "${!depnames[@]}" ; do
					[ "$token" = "${depnames[$i]}" ] && depname="${depnames[$i]}" && break
				done
			else
				for i in "${!depnums[@]}" ; do
					[ "$token" = "${depnums[$i]}" ] && depname="${depnames[$i]}" && break
				done
			fi
			[ -z "$depname" ] && { echo "Deployment \"$token\" does not exist!" >&2; exit 1; }
		fi
		# if no selection is done by now, allow interactive selection
		if [ -z "$pardet" ] ; then
			f_deplist
			echo "Select deployment by its order number (first number on line):"
			read -r resp
			if [[ -n "$resp" && "$resp" = "${resp//[^0-9]}" && "$resp" -gt 0 && "$resp" -le ${#depnums[@]} ]] ; then
				depname="${depnames[$((resp-1))]}"
			else
				echo "Invalid deployment selection, bailing out." >&2
				exit 1
			fi
		fi
		[ -z "$depname" ] && { echo "No deployment name!" >&2; exit 1; }
	fi
	f_"$func" "$depname" "$@"
else
	help "$@"
fi
