#!/bin/sh
# shellcheck shell=ash

# Variables are populated by sourced SafeShield helpers and ss_load_config().
# shellcheck disable=SC2154
LC_ALL=C
export LC_ALL
umask 077

SS_STATSD_FUNCTIONS_LIB="${SS_STATSD_FUNCTIONS_LIB:-/lib/functions.sh}"
SS_STATSD_CORE_LIB="${SS_STATSD_CORE_LIB:-/usr/lib/safeshield/core.sh}"
SS_STATSD_AWK_DIR="${SS_STATSD_AWK_DIR:-/usr/lib/safeshield/statistics}"
SS_STATSD_POLL_COMMAND="${SS_STATSD_POLL_COMMAND:-/usr/libexec/safeshield-stats-poll}"
SS_STATSD_IDENTITY_COMMAND="${SS_STATSD_IDENTITY_COMMAND:-/usr/libexec/safeshield-stats-identities}"
SS_STATSD_PERSISTENT_STATE_FILE="${SS_STATSD_PERSISTENT_STATE_FILE:-/etc/safeshield/statistics-state.tsv}"
SS_STATSD_PERSISTENT_JOURNAL_FILE="${SS_STATSD_PERSISTENT_JOURNAL_FILE:-/etc/safeshield/statistics-journal.tsv}"
SS_STATSD_SEQUENCE_FILE="${SS_STATSD_SEQUENCE_FILE:-/etc/safeshield/statistics-sequence.tsv}"
SS_STATSD_SEQUENCE_RESERVATION_SIZE="${SS_STATSD_SEQUENCE_RESERVATION_SIZE:-4096}"
SS_STATSD_UPLOAD_WINDOW_HOURS="${SS_STATSD_UPLOAD_WINDOW_HOURS:-2}"
SS_STATSD_PERSISTENT_INTERVAL_S="${SS_STATSD_PERSISTENT_INTERVAL_S:-3600}"
SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S="${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S:-604800}"

# shellcheck disable=SC1090
. "$SS_STATSD_FUNCTIONS_LIB"
# shellcheck disable=SC1090
. "$SS_STATSD_CORE_LIB"

stats_poller_pid=''
stats_awk_pid=''
stats_fifo=''
stats_lock_dir=''
stats_lock_owned=0

stats_new_generation_id() {
	local generation_id="${SS_STATSD_GENERATION_ID:-}"

	if [ -n "$generation_id" ]; then
		printf '%s\n' "$generation_id"
		return 0
	fi

	if [ -r /proc/sys/kernel/random/uuid ]; then
		IFS= read -r generation_id </proc/sys/kernel/random/uuid || generation_id=''
	fi

	if [ -z "$generation_id" ]; then
		generation_id="fallback-$(date +%s 2>/dev/null || printf '0')-$$"
	fi

	printf '%s\n' "$generation_id"
}

stats_process_running() {
	local pid="${1:-}"
	[ -n "$pid" ] || return 1
	kill -0 "$pid" 2>/dev/null
}

stats_cleanup() {
	trap - EXIT HUP INT TERM

	# Stop the low-frequency UBus poller first. awk then receives EOF and runs
	# its END block, which performs the final snapshot and persistence flush.
	if stats_process_running "$stats_poller_pid"; then
		kill "$stats_poller_pid" 2>/dev/null || true
	fi

	[ -n "$stats_poller_pid" ] && wait "$stats_poller_pid" 2>/dev/null || true
	stats_poller_pid=''

	[ -n "$stats_awk_pid" ] && wait "$stats_awk_pid" 2>/dev/null || true
	stats_awk_pid=''

	[ -n "$stats_fifo" ] && rm -f "$stats_fifo"

	if [ "$stats_lock_owned" -eq 1 ] && [ -n "$stats_lock_dir" ]; then
		rm -rf "$stats_lock_dir"
	fi
}

stats_terminate() {
	local rc="${1:-0}"
	stats_cleanup
	exit "$rc"
}

stats_acquire_lock() {
	local owner_pid=''

	stats_lock_dir="${SS_STATISTICS_DIR}/collector.lock"

	if mkdir "$stats_lock_dir" 2>/dev/null; then
		stats_lock_owned=1
		printf '%s\n' "$$" >"${stats_lock_dir}/pid"
		return 0
	fi

	if [ -r "${stats_lock_dir}/pid" ]; then
		owner_pid="$(cat "${stats_lock_dir}/pid" 2>/dev/null || true)"
	fi

	if stats_process_running "$owner_pid"; then
		log_warn "Statistics collector is already running (pid=${owner_pid})"
		return 1
	fi

	log_warn 'Removing stale statistics collector lock'
	rm -rf "$stats_lock_dir" || return 1
	mkdir "$stats_lock_dir" || return 1
	stats_lock_owned=1
	printf '%s\n' "$$" >"${stats_lock_dir}/pid"
	return 0
}

stats_poll_loop() {
	local snapshot_interval="$1"
	local poll_command="$2"
	local sleep_pid=''

	trap 'if [ -n "$sleep_pid" ]; then kill "$sleep_pid" 2>/dev/null || true; fi; exit 0' HUP INT TERM

	while :; do
		if ! "$poll_command"; then
			printf 'error\tdnsmasq_ubus_poll_failed\n'
		fi

		sleep "$snapshot_interval" &
		sleep_pid=$!
		wait "$sleep_pid" 2>/dev/null || true
		sleep_pid=''
	done
}

stats_run_collector() {
	local snapshot_interval="$1"
	local retention_hours="$2"
	local lease_file="$3"
	local persistent_state_file="$4"
	local persistent_journal_file="$5"
	local sequence_file="$6"
	local generation_id ipv6_neigh_command='' ipv6_identity_command='' ipv6_identity_file="${SS_STATSD_IPV6_IDENTITY_FILE:-}" force_rebaseline=0 rc=0

	if [ -f "${SS_STATISTICS_REBASELINE_FILE}" ]; then
		force_rebaseline=1
	fi

	# This is only a candidate for a brand-new dataset. The collector modules keep
	# the generation restored from tmpfs or persistent state when one already exists.
	generation_id="$(stats_new_generation_id)"
	if command_exists ip; then
		ipv6_neigh_command='ip -6 neigh show 2>/dev/null'
	fi
	if [ -z "$ipv6_identity_file" ] && [ -x "$SS_STATSD_IDENTITY_COMMAND" ]; then
		ipv6_identity_command="$SS_STATSD_IDENTITY_COMMAND 2>/dev/null"
	fi

	stats_fifo="${SS_STATISTICS_DIR}/events.$$"
	rm -f "$stats_fifo"
	mkfifo "$stats_fifo" || {
		log_error "Failed to create statistics FIFO: ${stats_fifo}"
		return 1
	}

	stats_poll_loop "$snapshot_interval" "$SS_STATSD_POLL_COMMAND" >"$stats_fifo" &
	stats_poller_pid=$!

	awk \
		-v state_file="${SS_STATISTICS_STATE_FILE}" \
		-v json_file="${SS_STATISTICS_JSON_FILE}" \
		-v upload_json_file="${SS_STATISTICS_UPLOAD_JSON_FILE}" \
		-v upload_window_hours="${SS_STATSD_UPLOAD_WINDOW_HOURS}" \
		-v persistent_state_file="${persistent_state_file}" \
		-v persistent_journal_file="${persistent_journal_file}" \
		-v sequence_file="${sequence_file}" \
		-v snapshot_seq_reservation_size="${SS_STATSD_SEQUENCE_RESERVATION_SIZE}" \
		-v persistent_interval="${SS_STATSD_PERSISTENT_INTERVAL_S}" \
		-v persistent_compact_interval="${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S}" \
		-v snapshot_interval="${snapshot_interval}" \
		-v retention_hours="${retention_hours}" \
		-v lease_file="${lease_file}" \
		-v ipv6_neigh_command="${ipv6_neigh_command}" \
		-v ipv6_identity_file="${ipv6_identity_file}" \
		-v ipv6_identity_command="${ipv6_identity_command}" \
		-v lease_refresh_interval=60 \
		-v identity_cache_ttl=60 \
		-v max_devices=128 \
		-v generation_seed="${generation_id}" \
		-v force_rebaseline="${force_rebaseline}" \
		-v rebaseline_file="${SS_STATISTICS_REBASELINE_FILE}" \
		-f "$SS_STATSD_AWK_DIR/00-common.awk" \
		-f "$SS_STATSD_AWK_DIR/10-recovery.awk" \
		-f "$SS_STATSD_AWK_DIR/20-identity.awk" \
		-f "$SS_STATSD_AWK_DIR/30-aggregate.awk" \
		-f "$SS_STATSD_AWK_DIR/40-persistence.awk" \
		-f "$SS_STATSD_AWK_DIR/50-output.awk" \
		-f "$SS_STATSD_AWK_DIR/90-main.awk" \
		<"$stats_fifo" &
	stats_awk_pid=$!

	wait "$stats_awk_pid" || rc=$?
	stats_awk_pid=''

	if stats_process_running "$stats_poller_pid"; then
		kill "$stats_poller_pid" 2>/dev/null || true
	fi
	wait "$stats_poller_pid" 2>/dev/null || true
	stats_poller_pid=''

	return "$rc"
}

main() {
	local snapshot_interval configured_snapshot_interval retention_hours lease_file configured_lease_file
	local persistent_state_file persistent_journal_file sequence_file persistence_mode rc=0

	ss_load_config || return 1
	[ "${ss_enabled}" = "1" ] || return 0
	[ "${ss_statistics_enabled}" = "1" ] || return 0

	if ! ss_dnsmasq_safeshield_block_supported; then
		log_warn 'SafeShield statistics collector is disabled because dnsmasq does not provide the SafeShield extension'
		return 0
	fi

	[ -x "$SS_STATSD_POLL_COMMAND" ] || {
		log_error "SafeShield statistics poll command is unavailable: ${SS_STATSD_POLL_COMMAND}"
		return 1
	}
	command_exists mkfifo || {
		log_error 'mkfifo is required for SafeShield statistics'
		return 1
	}

	persistent_state_file="${SS_STATSD_PERSISTENT_STATE_FILE}"
	persistent_journal_file="${SS_STATSD_PERSISTENT_JOURNAL_FILE}"
	sequence_file="${SS_STATSD_SEQUENCE_FILE}"
	persistence_mode='journal'
	if ! ss_statistics_persistence_enabled; then
		persistent_state_file=''
		persistent_journal_file=''
		sequence_file=''
		persistence_mode='volatile'
	fi

	mkdir -p "${SS_STATISTICS_DIR}" || return 1
	if [ -n "$persistent_state_file" ]; then
		mkdir -p "${persistent_state_file%/*}" || return 1
	fi
	if [ -n "$persistent_journal_file" ]; then
		mkdir -p "${persistent_journal_file%/*}" || return 1
	fi
	if [ -n "$sequence_file" ]; then
		mkdir -p "${sequence_file%/*}" || return 1
	fi
	stats_acquire_lock || return 0

	trap 'stats_cleanup' EXIT
	trap 'stats_terminate 129' HUP
	trap 'stats_terminate 130' INT
	trap 'stats_terminate 143' TERM

	configured_snapshot_interval="${ss_statistics_snapshot_interval_s}"
	snapshot_interval="$(ss_statistics_effective_snapshot_interval "$configured_snapshot_interval")"
	retention_hours="${ss_statistics_retention_hours}"
	lease_file="${SS_STATSD_DHCP_LEASE_FILE:-/tmp/dhcp.leases}"
	if [ -z "${SS_STATSD_DHCP_LEASE_FILE:-}" ] && command_exists uci; then
		configured_lease_file="$(uci -q get dhcp.@dnsmasq[0].leasefile 2>/dev/null || true)"
		[ -n "$configured_lease_file" ] && lease_file="$configured_lease_file"
	fi

	log_info "Starting statistics collector (source=dnsmasq-ubus snapshot=${snapshot_interval}s configured_snapshot=${configured_snapshot_interval}s retention=${retention_hours}h persistence=${persistence_mode} journal=${SS_STATSD_PERSISTENT_INTERVAL_S}s compact=${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S}s)"

	stats_run_collector \
		"$snapshot_interval" \
		"$retention_hours" \
		"$lease_file" \
		"$persistent_state_file" \
		"$persistent_journal_file" \
		"$sequence_file" || rc=$?
	return "$rc"
}

main "$@"
