#!/bin/sh

. /lib/functions.sh

day=""
next_days=""
prev_days=""
schedule_added=""

ACCESS_RULE=""
IP_RULE=""

ACL_FILE=""
parentalcontrol_ipv4_forward=""
parentalcontrol_ipv6_forward=""

bundle_path="$(uci -q get parentalcontrol.globals.bundle_path)"

default_bundle_dir="${bundle_path}/default/"
bundle_archive="/etc/parentalcontrol/urlbundles.tar.xz"

log() {
	echo "$*" |logger -t urlfilter.init -p debug
}

process_default_bundles() {
	if [ -s "$bundle_archive" ]; then
		if mkdir -p "$default_bundle_dir"; then
			if tar -xJf "$bundle_archive" -C "$default_bundle_dir"; then
				log "default bundles placed at $default_bundle_dir"
			else
				log "default bundles could not be placed at $default_bundle_dir"
			fi
		else
			log "could not create directory: $default_bundle_dir"
		fi
	else
		log "default bundles not available"
	fi
}

remove_default_bundles() {
	rm -rf "$default_bundle_dir"
}

# Function to calculate UTC time and relative day
get_relative_day() {
	local hour="$1"
	local offset="$2"
	local relative_day="$3"
	local utc_hour

	# we need to force hours and minutes to be treated as base 10 (decimal)
	# otherwise shell will treat, for example, 09 as octal
	# hour=$((10#$hour)) does not work on busybox
	# so we use another trick
	hour=$(expr $hour + 0)

	# Extract the sign and the hour part of the offset
	local sign=${offset:0:1}
	local offset_hour=${offset:1:2}

	# Adjust hour based on the offset
	if [ "$sign" = "-" ]; then
		utc_hour=$((hour + offset_hour))
	else
		utc_hour=$((hour - offset_hour))
	fi

	# Handle overflow/underflow of UTC hours to keep within 0-23 range
	if [ $utc_hour -lt 0 ]; then
		if [ "$relative_day" = "today" ]; then
			relative_day="yesterday"
		else
			relative_day="today"
		fi
	elif [ $utc_hour -ge 24 ]; then
		if [ "$relative_day" = "today" ]; then
			relative_day="tomorrow"
		else
			relative_day="tomorrow"
		fi
	else
		if [ "$relative_day" = "tomorrow" ]; then
			relative_day="tomorrow"
		else
			relative_day="today"
		fi
	fi

	echo "$relative_day"
}

get_next_day() {
	local weekday="$1"
	case "$weekday" in
		"Mon"|"Monday") echo "Tuesday"
		;;
		"Tue"|"Tuesday") echo "Wednesday"
		;;
		"Wed"|"Wednesday") echo "Thursday"
		;;
		"Thu"|"Thursday") echo "Friday"
		;;
		"Fri"|"Friday") echo "Saturday"
		;;
		"Sat"|"Saturday") echo "Sunday"
		;;
		"Sun"|"Sunday") echo "Monday"
		;;
	esac
}

get_previous_day() {
	local weekday="$1"
	case "$weekday" in
		"Mon"|"Monday") echo "Sunday"
		;;
		"Tue"|"Tuesday") echo "Monday"
		;;
		"Wed"|"Wednesday") echo "Tuesday"
		;;
		"Thu"|"Thursday") echo "Wednesday"
		;;
		"Fri"|"Friday") echo "Thursday"
		;;
		"Sat"|"Saturday") echo "Friday"
		;;
		"Sun"|"Sunday") echo "Saturday"
		;;
	esac
}

add_access_rule() {
	local rule_prefix="$1"
	local start_time="$2"
	local stop_time="$3"
	local weekdays="$4"
	local target="$5"
	local rule
	local start_hm stop_hm

	if [ -z "$target" ]; then
		return
	fi

	if [ -n "$weekdays" ]; then

		start_hm=$(echo "$start_time" | awk -F: '{ print $1,$2 }' | sed 's/ //')
		stop_hm=$(echo "$stop_time" | awk -F: '{ print $1,$2 }' | sed 's/ //')

		if [ "$start_hm" = "$stop_hm" ]; then
			return
		fi

		rule_prefix="$rule_prefix -m time --timestart $start_time --timestop $stop_time --weekdays $weekdays"
	fi

	rule="$rule_prefix -j $target"

	echo "iptables -w -A parentalcontrol_forward ${rule}" >> "$ACL_FILE"
        echo "ip6tables -w -A parentalcontrol_forward ${rule}" >> "$ACL_FILE"
}

generate_ip_rule() {
	local utc_start_relative_day="$1"
	local utc_end_relative_day="$2"
	local utc_start_time="$3"
	local utc_stop_time="$4"
	local target="$5"

	# Handle the cases based on the relation between utc_start_relative_day and utc_end_relative_day
	if [ "$utc_start_relative_day" = "yesterday" ] && [ "$utc_end_relative_day" = "yesterday" ]; then
		# Rule for yesterday only
		add_access_rule "$IP_RULE" "$utc_start_time" "$utc_stop_time" "$prev_days" "$target"

	elif [ "$utc_start_relative_day" = "yesterday" ] && [ "$utc_end_relative_day" = "today" ]; then
		# Rule for yesterday to today
		add_access_rule "$IP_RULE" "$utc_start_time" "23:59:59" "$prev_days" "$target"
		add_access_rule "$IP_RULE" "00:00" "$utc_stop_time" "$day" "$target"

	elif [ "$utc_start_relative_day" = "today" ] && [ "$utc_end_relative_day" = "today" ]; then
		# Rule for today only
		add_access_rule "$IP_RULE" "$utc_start_time" "$utc_stop_time" "$day" "$target"

	elif [ "$utc_start_relative_day" = "today" ] && [ "$utc_end_relative_day" = "tomorrow" ]; then
		# Rule for today to tomorrow
		add_access_rule "$IP_RULE" "$utc_start_time" "23:59:59" "$day" "$target"
		add_access_rule "$IP_RULE" "00:00" "$utc_stop_time" "$next_days" "$target"

	elif [ "$utc_start_relative_day" = "tomorrow" ] && [ "$utc_end_relative_day" = "tomorrow" ]; then
		# Rule for tomorrow only
		add_access_rule "$IP_RULE" "$utc_start_time" "$utc_stop_time" "$next_days" "$target"
	else
		log "Error: Unhandled case"
	fi
}

handle_day_list() {
	local value=$1

	val=$(echo $value | cut -c 1-3)
	next_day_val=$(get_next_day $val)
	prev_day_val=$(get_previous_day $val)
	if [ -z $day ]; then
		day="$val"
		next_days="$next_day_val"
		prev_days="$prev_day_val"
	else
		day="$day,$val"
		next_days="$next_days,$next_day_val"
		prev_days="$prev_days,$prev_day_val"
	fi
}

handle_schedule() {
	local schedule_section="$1"
	local type="$2"
	local schedule_ref="$3"
	local local_start_time local_stop_time duration zone_offset local_start_hh local_stop_hh
	local is_enabled
	local target
	local day_config
	local relative_day_end="today"

	IP_RULE="$ACCESS_RULE"
	day=""
	next_days=""
	prev_days=""
	local all_days="Monday Tuesday Wednesday Thursday Friday Saturday Sunday"

	zone_offset=$(date +%z)

	config_get_bool is_enabled "$schedule_section" "enable" 0
	if [ $is_enabled -eq 0 ]; then
		return
	fi

	if [ "$type" = "profile_bedtime_schedule" ]; then
		target="DROP"

		config_get local_start_time "$schedule_section" "start_time" "00:00:00"
		config_get local_stop_time "$schedule_section" "end_time" "23:59:59"

		local_start_hh=$(echo $local_start_time | awk -F: '{ print $1 }')
		local_stop_hh=$(echo $local_stop_time | awk -F: '{ print $1 }')

		config_get day_config "$schedule_section" "day" "$all_days"
	else
		if [ "$schedule_ref" != "$schedule_section" ]; then
			return
		fi

		# for access rules to be effective for a schedule, need to add DROP rule
		# to block the access outside the defined schedule
		# therefore, set flag
		if [ "$schedule_added" = "0" ]; then
			schedule_added="1"
		fi

		# internet_access has been updated to be internet_break
		# so drop traffic during the schedule, and allow outside the schedule
		target="DROP"

		config_get local_start_time "$schedule_section" "start_time" "00:00"
		config_get duration "$schedule_section" "duration"

		local hh=$(echo $local_start_time | awk -F: '{ print $1 }')
		local mm=$(echo $local_start_time | awk -F: '{ print $2 }')
		local hh_s=`expr $hh \* 3600`
		local mm_s=`expr $mm \* 60`
		local ss=$(( hh_s + mm_s ))
		local_start_hh=$hh

		if [ -n "$duration" ]; then
			local stop_ss rem_ss mm
			stop_ss=$(( ss + duration ))
			hh=$(( stop_ss / 3600 ))
			rem_ss=$(( stop_ss % 3600 ))
			mm=$(( rem_ss / 60 ))
			ss=$(( rem_ss % 60 ))
			local_stop_time="$hh:$mm:$ss"
			local_stop_hh="$hh"
		else
			# if duration is not specified, then apply rule to end of the day
			local_stop_time="23:59:59"
			local_stop_hh="23"
		fi

		config_get day_config "$schedule_section" "day" "$all_days"
	fi

	IFS=" "
	for d in $day_config; do
		handle_day_list $d
	done

	utc_start_time=$(date -u -d @$(date "+%s" -d "$local_start_time") +%H:%M)
	utc_start_time="$utc_start_time"
	utc_stop_time=$(date -u -d @$(date "+%s" -d "$local_stop_time") +%H:%M)
	utc_stop_time="$utc_stop_time"

	# Determine whether the local end hour crosses midnight
	if [ "$local_start_hh" -gt "$local_stop_hh" ]; then
		relative_day_end="tomorrow"
	fi

	local utc_start_relative_day=$(get_relative_day "$local_start_hh" "$zone_offset" "today")
	local utc_end_relative_day=$(get_relative_day "$local_stop_hh" "$zone_offset" "$relative_day_end")

	generate_ip_rule "$utc_start_relative_day" "$utc_end_relative_day" "$utc_start_time" "$utc_stop_time" "$target"
}

handle_bedtime() {
	local mac_addresses="$1"
	local mac

	# if mac addresses are present, then we apply the rule for each mac address
	# otherwise apply the rule to everybody
	for mac in $mac_addresses; do
		ACCESS_RULE="-m mac --mac-source $mac"

		config_foreach handle_schedule profile_bedtime_schedule "profile_bedtime_schedule" ""
	done
}

handle_internet_break() {
	local mac_addresses="$1"
	local mac

	local schedule_ref
	config_get schedule_ref "$profile_section" "internet_break_schedule"

	for mac in $mac_addresses; do
		ACCESS_RULE="-m mac --mac-source $mac"

		schedule_added="0"

		# check if schedule is defined for this profile/internet_break instance
		# and if yes, create rule accordingly
		if [ -n "$schedule_ref" ]; then
			config_load "schedules"
			config_foreach handle_schedule schedule "schedule" "$schedule_ref"
		fi
	done
}

parse_macs() {
	local maclist="$1"
	local m mac

	for m in $maclist; do
		# trim whitespace
		mac="$(echo "$m" | tr -d ' \t\r\n')"

		# validate format
		if echo "$mac" | grep -qE '^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$'; then
			echo "$mac"
		else
			log "parse_macs(): Invalid MAC in mac list: '$mac'"
		fi
	done
}

handle_profile() {
	local profile_section="$1"
	local internet_break_enable bedtime_enable hostlist maclist

        config_get_bool internet_break_enable "$profile_section" "internet_break_enable" 0
        config_get_bool bedtime_enable "$profile_section" "bedtime_enable" 0

        if [ $internet_break_enable -eq 0 ] && [ $bedtime_enable -eq 0 ]; then
		return
	fi

        config_get hostlist "$profile_section" "host"
        config_get maclist  "$profile_section" "mac"

        # If both lists are empty, nothing to do
        if [ -z "$hostlist" ] && [ -z "$maclist" ]; then
                return
        fi

        ACCESS_RULE=""

	# both uci options contain mac addresses
	# one is given directly by the user
	# other is resolved by the data model from Hosts.Host object
        local mac_addresses="$(parse_macs "${hostlist} ${maclist}" | awk '{ if (NF && !seen[$0]++) { print $0 } }' | tr '\n' ' ')"

        # default value of Hosts.AccessControl.{i}.Enable is false,
        # so, if not defined in uci as 1, assume 0
        if [ $internet_break_enable -gt 0 ]; then
                handle_internet_break "${mac_addresses}"
                # handle_internet_break may have loaded schedules uci
                # so, reload parentalcontrol
                config_load "parentalcontrol"
        fi

        if [ $bedtime_enable -gt 0 ]; then
                handle_bedtime "${mac_addresses}"
        fi
}

add_internet_schedule_rules() {
	ACL_FILE="/tmp/parentalcontrol_access_control/access_control.rules"

	rm -f $ACL_FILE

	mkdir -p /tmp/parentalcontrol_access_control/
	touch $ACL_FILE

	echo "iptables -w -F parentalcontrol_forward" >> $ACL_FILE
	echo "ip6tables -w -F parentalcontrol_forward" >> $ACL_FILE

	parentalcontrol_ipv4_forward=$(iptables -w -t filter --list -n | grep parentalcontrol_forward)
	if [ -z "$parentalcontrol_ipv4_forward" ]; then
		echo "iptables -w -t filter -N parentalcontrol_forward" >> $ACL_FILE
		ret=$?
		[ $ret -eq 0 ] && echo "iptables -w -t filter -I FORWARD -j parentalcontrol_forward" >> $ACL_FILE
	fi

	parentalcontrol_ipv6_forward=$(ip6tables -w -t filter --list -n | grep parentalcontrol_forward)
	if [ -z "$parentalcontrol_ipv6_forward" ]; then
		echo "ip6tables -w -t filter -N parentalcontrol_forward" >> $ACL_FILE
		ret=$?
		[ $ret -eq 0 ] && echo "ip6tables -w -t filter -I FORWARD -j parentalcontrol_forward" >> $ACL_FILE
	fi

	# Load /etc/config/parentalcontrol UCI file
	config_load "parentalcontrol"
	config_foreach handle_profile "profile"

	# apply the rules
	sh $ACL_FILE
}

add_iptables_nfqueue_rules() {
    local queue_num="$1"

    # Check if urlfilter used
    if ! uci show parentalcontrol | grep -q profile_urlfilter; then
        return
    fi

    # IPv4
    # FORWARD
    if ! iptables -w -nL | grep -q "URLFILTER_FORWARD"; then
        iptables -w -N URLFILTER_FORWARD
        iptables -w -I FORWARD 1 -j URLFILTER_FORWARD

        # capture DNS responses (sport 53)
        iptables -w -A URLFILTER_FORWARD -p tcp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass
        iptables -w -A URLFILTER_FORWARD -p udp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass

        # HTTP/HTTPS flows
        iptables -w -A URLFILTER_FORWARD -p tcp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
        iptables -w -A URLFILTER_FORWARD -p udp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
    fi

    # INPUT
    if ! iptables -w -nL | grep -q "URLFILTER_INPUT"; then
        iptables -w -N URLFILTER_INPUT
        iptables -w -I INPUT 1 -j URLFILTER_INPUT

        iptables -w -A URLFILTER_INPUT -p tcp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
        iptables -w -A URLFILTER_INPUT -p udp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
    fi

    # OUTPUT
    if ! iptables -w -nL | grep -q "URLFILTER_OUTPUT"; then
        iptables -w -N URLFILTER_OUTPUT
        iptables -w -I OUTPUT 1 -j URLFILTER_OUTPUT

        iptables -w -A URLFILTER_OUTPUT -p tcp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
        iptables -w -A URLFILTER_OUTPUT -p udp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
    fi

    # ebtables bypass for IPv4
    ebtables --concurrent -A FORWARD -p ip --ip-protocol 6  --ip-destination-port 443 -j SKIPLOG 2>/dev/null
    ebtables --concurrent -A FORWARD -p ip --ip-protocol 6  --ip-source-port 53    -j SKIPLOG 2>/dev/null
    ebtables --concurrent -A FORWARD -p ip --ip-protocol 17 --ip-source-port 53    -j SKIPLOG 2>/dev/null

    # IPv6
    # FORWARD
    if ! ip6tables -w -nL | grep -q "URLFILTER_FORWARD6"; then
        ip6tables -w -N URLFILTER_FORWARD6
        ip6tables -w -I FORWARD 1 -j URLFILTER_FORWARD6

        ip6tables -w -A URLFILTER_FORWARD6 -p tcp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass
        ip6tables -w -A URLFILTER_FORWARD6 -p udp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass

        ip6tables -w -A URLFILTER_FORWARD6 -p tcp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
        ip6tables -w -A URLFILTER_FORWARD6 -p udp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
    fi

    # INPUT
    if ! ip6tables -w -nL | grep -q "URLFILTER_INPUT6"; then
        ip6tables -w -N URLFILTER_INPUT6
        ip6tables -w -I INPUT 1 -j URLFILTER_INPUT6

        ip6tables -w -A URLFILTER_INPUT6 -p tcp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
        ip6tables -w -A URLFILTER_INPUT6 -p udp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
    fi

    # OUTPUT
    if ! ip6tables -w -nL | grep -q "URLFILTER_OUTPUT6"; then
        ip6tables -w -N URLFILTER_OUTPUT6
        ip6tables -w -I OUTPUT 1 -j URLFILTER_OUTPUT6

        ip6tables -w -A URLFILTER_OUTPUT6 -p tcp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
        ip6tables -w -A URLFILTER_OUTPUT6 -p udp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
    fi

    # ebtables bypass for IPv6
    ebtables --concurrent -A FORWARD -p ip6 --ip6-protocol 6  --ip6-destination-port 443 -j SKIPLOG 2>/dev/null
    ebtables --concurrent -A FORWARD -p ip6 --ip6-protocol 6  --ip6-source-port 53    -j SKIPLOG 2>/dev/null
    ebtables --concurrent -A FORWARD -p ip6 --ip6-protocol 17 --ip6-source-port 53    -j SKIPLOG 2>/dev/null
}

remove_iptables_nfqueue_rules() {
    # IPv4
    for chain in URLFILTER_FORWARD URLFILTER_INPUT URLFILTER_OUTPUT; do
        if iptables -w -nL | grep -q "$chain"; then
            iptables -w -D FORWARD -j $chain 2>/dev/null
            iptables -w -D INPUT   -j $chain 2>/dev/null
            iptables -w -D OUTPUT  -j $chain 2>/dev/null
            iptables -w -F $chain
            iptables -w -X $chain
        fi
    done

    ebtables --concurrent -D FORWARD -p ip --ip-protocol 6  --ip-destination-port 443 -j SKIPLOG 2>/dev/null
    ebtables --concurrent -D FORWARD -p ip --ip-protocol 6  --ip-source-port 53    -j SKIPLOG 2>/dev/null
    ebtables --concurrent -D FORWARD -p ip --ip-protocol 17 --ip-source-port 53    -j SKIPLOG 2>/dev/null

    # IPv6
    for chain in URLFILTER_FORWARD6 URLFILTER_INPUT6 URLFILTER_OUTPUT6; do
        if ip6tables -w -nL | grep -q "$chain"; then
            ip6tables -w -D FORWARD -j $chain 2>/dev/null
            ip6tables -w -D INPUT   -j $chain 2>/dev/null
            ip6tables -w -D OUTPUT  -j $chain 2>/dev/null
            ip6tables -w -F $chain
            ip6tables -w -X $chain
        fi
    done

    ebtables --concurrent -D FORWARD -p ip6 --ip6-protocol 6  --ip6-destination-port 443 -j SKIPLOG 2>/dev/null
    ebtables --concurrent -D FORWARD -p ip6 --ip6-protocol 6  --ip6-source-port 53    -j SKIPLOG 2>/dev/null
    ebtables --concurrent -D FORWARD -p ip6 --ip6-protocol 17 --ip6-source-port 53    -j SKIPLOG 2>/dev/null
}

remove_internet_schedule_rules() {
	# remove from iptables, if chain exists
	if iptables -w -nL FORWARD|grep -iqE "parentalcontrol_forward"; then
		iptables -w -t filter -D FORWARD -j parentalcontrol_forward
		iptables -w -F parentalcontrol_forward
		iptables -w -X parentalcontrol_forward
	fi
	# remove from ip6tables, if chain exists
	if ip6tables -w -nL FORWARD|grep -iqE "parentalcontrol_forward"; then
		ip6tables -w -t filter -D FORWARD -j parentalcontrol_forward
		ip6tables -w -F parentalcontrol_forward
		ip6tables -w -X parentalcontrol_forward
	fi
}

get_host_ip_from_mac() {
	local mac="$1"
	local ip=""

	# Validate MAC format
	if ! echo "$mac" | grep -qE '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'; then
		log "get_host_ip_from_mac(): Invalid MAC address format '$mac'"
		return 1
	fi

	# Try to find IP from ARP table
	ip="$(cat /proc/net/arp | awk -v mac="$mac" 'tolower($4) == tolower(mac) {print $1; exit}')"

	if [ -n "$ip" ]; then
		URLFILTER_IPS="${URLFILTER_IPS} ${ip}"
		return 0
	else
		log "get_host_ip_from_mac(): No IP found for MAC $mac in ARP table"
		return 1
	fi
}

# Global array for resolved IPs
URLFILTER_IPS=""

resolve_profile_hosts() {
	local section="$1"
	local hostlist maclist h m

	config_get hostlist "$section" host
	config_get maclist  "$section" mac

	for h in $hostlist; do
		get_host_ip_from_mac "$h"
	done

	for m in $maclist; do
		get_host_ip_from_mac "$m"
	done
}

# Main function to collect IPs and delete conntrack entries
flush_conntrack_for_hosts() {
	URLFILTER_IPS=""
	local count max

	config_foreach resolve_profile_hosts profile

	URLFILTER_IPS="$(echo "$URLFILTER_IPS" | tr ' ' '\n' | sort -u | xargs)"
	for ip in $URLFILTER_IPS; do
		count=0
		max=1000
		while conntrack -D -s "$ip" >/dev/null 2>&1; do
			count=$((count+1))
			if [ $count -ge $max ]; then
				log "Warning: Forced to stop conntrack delete after $max deletions for $ip (possible loop)"
				break
			fi
		done
	done
}

OVERRIDE_JSON="/etc/parentalcontrol/urlbundle_override.json"
DM_PLUGIN_PATH="/usr/share/bbfdm/micro_services/parentalcontrol/urlbundle_override.json"

enable_urlfilter_dm() {
	if [ -f "${DM_PLUGIN_PATH}" ]; then
		rm ${DM_PLUGIN_PATH}
		echo "Please restart to apply"
	fi
}

disable_urlfilter_dm() {
	mkdir -p "$(dirname ${DM_PLUGIN_PATH})"

	if [ ! -f "${DM_PLUGIN_PATH}" ]; then
		if [ -f "${OVERRIDE_JSON}" ]; then
			cp "${OVERRIDE_JSON}" "${DM_PLUGIN_PATH}"
			echo "Please restart to apply"

		fi
	fi
}
