#!/bin/sh /etc/rc.common

START=99
STOP=10
USE_PROCD=1

. "$IPKG_INSTROOT/lib/functions/network.sh"
. "$IPKG_INSTROOT/etc/nikki/scripts/include.sh"

extra_command 'update_subscription' 'Update subscription by section id'

boot() {
	# prepare files
	prepare_files
	# load config
	config_load nikki
	# start delay
	local enabled start_delay
	config_get_bool enabled "config" "enabled" 0
	config_get start_delay "config" "start_delay" 0
	if [ "$enabled" = 1 ] && [ "$start_delay" -gt 0 ]; then
		log "App" "Start after $start_delay seconds."
		sleep "$start_delay"
	fi
	# start
	start
}

start_service() {
	# prepare files
	prepare_files
	# load config
	config_load nikki
	# check if enabled
	local enabled
	config_get_bool enabled "config" "enabled" 0
	if [ "$enabled" = 0 ]; then
		log "App" "Disabled."
		log "App" "Exit."
		return
	fi
	# start
	log "App" "Enabled."
	log "App" "Start."
	# get config
	## app config
	local scheduled_restart cron_expression profile test_profile fast_reload core_only
	config_get_bool scheduled_restart "config" "scheduled_restart" 0
	config_get cron_expression "config" "cron_expression"
	config_get profile "config" "profile"
	config_get_bool test_profile "config" "test_profile" 0
	config_get_bool fast_reload "config" "fast_reload" 0
	config_get_bool core_only "config" "core_only" 0
	## mixin config
	### overwrite
	local overwrite_authentication overwrite_tun_dns_hijack overwrite_fake_ip_filter overwrite_hosts overwrite_dns_nameserver overwrite_dns_nameserver_policy overwrite_sniffer_sniff overwrite_sniffer_force_domain_name overwrite_sniffer_ignore_domain_name
	config_get_bool overwrite_authentication "mixin" "authentication" 0
	config_get_bool overwrite_tun_dns_hijack "mixin" "tun_dns_hijack" 0
	config_get_bool overwrite_fake_ip_filter "mixin" "fake_ip_filter" 0
	config_get_bool overwrite_hosts "mixin" "hosts" 0
	config_get_bool overwrite_dns_nameserver "mixin" "dns_nameserver" 0
	config_get_bool overwrite_dns_nameserver_policy "mixin" "dns_nameserver_policy" 0
	config_get_bool overwrite_sniffer_force_domain_name "mixin" "sniffer_force_domain_name" 0
	config_get_bool overwrite_sniffer_ignore_domain_name "mixin" "sniffer_ignore_domain_name" 0
	config_get_bool overwrite_sniffer_sniff "mixin" "sniffer_sniff" 0
	### mixin file content
	local mixin_file_content
	config_get_bool mixin_file_content "mixin" "mixin_file_content" 0
	## environment variable
	local safe_paths disable_loopback_detector disable_quic_go_gso disable_quic_go_ecn skip_system_ipv6_check
	config_get safe_paths "env" "safe_paths" 
	config_get_bool disable_loopback_detector "env" "disable_loopback_detector" 0
	config_get_bool disable_quic_go_gso "env" "disable_quic_go_gso" 0
	config_get_bool disable_quic_go_ecn "env" "disable_quic_go_ecn" 0
	config_get_bool skip_system_ipv6_check "env" "skip_system_ipv6_check" 0
	# get profile
	local profile_type; profile_type=$(echo "$profile" | cut -d ':' -f 1)
	local profile_id; profile_id=$(echo "$profile" | cut -d ':' -f 2)
	if [ "$profile_type" = "file" ]; then
		local profile_name; profile_name="$profile_id"
		local profile_file; profile_file="$PROFILES_DIR/$profile_name"
		log "Profile" "Use file: $profile_name."
		if [ ! -f "$profile_file" ]; then
			log "Profile" "File not found."
			log "App" "Exit."
			return
		fi
		cp -f "$profile_file" "$RUN_PROFILE_PATH"
	elif [ "$profile_type" = "subscription" ]; then
		local subscription_section; subscription_section="$profile_id"
		local subscription_name subscription_prefer
		config_get subscription_name "$subscription_section" "name"
		config_get subscription_prefer "$subscription_section" "prefer" "remote"
		log "Profile" "Use subscription: $subscription_name."
		local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml"
		if [ "$subscription_prefer" = "remote" ] || [ ! -f "$subscription_file" ]; then
			update_subscription "$subscription_section"
		fi
		if [ ! -f "$subscription_file" ]; then
			log "Profile" "Subscription file not found."
			log "App" "Exit."
			return
		fi
		cp -f "$subscription_file" "$RUN_PROFILE_PATH"
	else
		log "Profile" "No profile/subscription selected."
		log "App" "Exit."
		return
	fi
	# mixin
	if [ "$core_only" = 0 ]; then
		log "Mixin" "Mixin config."
		if [ "$overwrite_authentication" = 1 ]; then
			yq -M -i 'del(.authentication)' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_tun_dns_hijack" = 1 ]; then
			yq -M -i 'del(.tun.dns-hijack)' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_fake_ip_filter" = 1 ]; then
			yq -M -i 'del(.dns.fake-ip-filter)' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_hosts" = 1 ]; then
			yq -M -i 'del(.hosts)' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_dns_nameserver" = 1 ]; then
			yq -M -i 'del(.dns.default-nameserver) | del(.dns.proxy-server-nameserver) | del(.dns.direct-nameserver) | del(.dns.nameserver) | del(.dns.fallback) ' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_dns_nameserver_policy" = 1 ]; then
			yq -M -i 'del(.dns.nameserver-policy)' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_sniffer_force_domain_name" = 1 ]; then
			yq -M -i 'del(.sniffer.force-domain)' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_sniffer_ignore_domain_name" = 1 ]; then
			yq -M -i 'del(.sniffer.skip-domain)' "$RUN_PROFILE_PATH"
		fi
		if [ "$overwrite_sniffer_sniff" = 1 ]; then
			yq -M -i 'del(.sniffer.sniff)' "$RUN_PROFILE_PATH"
		fi
		if [ "$mixin_file_content" = 0 ]; then
			ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" -
		elif [ "$mixin_file_content" = 1 ]; then
			ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" "$MIXIN_FILE_PATH" -
		fi
	fi
	# test profile
	if [ "$test_profile" = 1 ]; then
		log "Profile" "Testing..."
		if ($PROG -d "$RUN_DIR" -t >> "$CORE_LOG_PATH" 2>&1); then
			log "Profile" "Test passed."
		else
			log "Profile" "Test failed."
			log "Profile" "Please check the core log to find out the problem."
			log "App" "Exit."
			return
		fi
	fi
	# start core
	log "Core" "Start."
	procd_open_instance nikki

	procd_set_param command /bin/sh -c "$PROG -d $RUN_DIR >> $CORE_LOG_PATH 2>&1"
	procd_set_param file "$RUN_PROFILE_PATH"
	procd_append_param env SAFE_PATHS="$safe_paths"
	procd_append_param env DISABLE_LOOPBACK_DETECTOR="$disable_loopback_detector"
	procd_append_param env QUIC_GO_DISABLE_GSO="$disable_quic_go_gso"
	procd_append_param env QUIC_GO_DISABLE_ECN="$disable_quic_go_ecn"
	procd_append_param env SKIP_SYSTEM_IPV6_CHECK="$skip_system_ipv6_check"
	if [ "$fast_reload" = 1 ]; then
		procd_set_param reload_signal HUP
	fi
	procd_set_param pidfile "$PID_FILE_PATH"
	procd_set_param respawn
	procd_append_param limits core="unlimited"
	procd_append_param limits nofile="1048576 1048576"

	procd_close_instance
	# cron
	if [ "$scheduled_restart" = 1 ] && [ -n "$cron_expression" ]; then
		log "App" "Set scheduled restart."
		echo "$cron_expression /etc/init.d/nikki restart #nikki" >> "/etc/crontabs/root"
		/etc/init.d/cron restart
	fi
	# set started flag
	touch "$STARTED_FLAG_PATH"
}

service_started() {
	# check if started
	if [ ! -f "$STARTED_FLAG_PATH" ]; then
		return
	fi
	# load config
	config_load nikki
	# check if proxy enabled
	local enabled
	config_get_bool enabled "proxy" "enabled" 0
	if [ "$enabled" = 0 ]; then
		log "Proxy" "Disabled."
		return
	fi
	# get config
	## app config
	local core_only
	config_get_bool core_only "config" "core_only" 0
	## mixin
	### tun
	local tun_device
	config_get tun_device "mixin" "tun_device" "nikki"
	## proxy config
	### general
	local tcp_mode udp_mode ipv4_proxy ipv6_proxy tun_timeout tun_interval
	config_get tcp_mode "proxy" "tcp_mode"
	config_get udp_mode "proxy" "udp_mode"
	config_get_bool ipv4_proxy "proxy" "ipv4_proxy" 0
	config_get_bool ipv6_proxy "proxy" "ipv6_proxy" 0
	config_get tun_timeout "proxy" "tun_timeout" 30
	config_get tun_interval "proxy" "tun_interval" 1
	## routing config
	local tproxy_fw_mark tun_fw_mark tproxy_rule_pref tun_rule_pref tproxy_route_table tun_route_table cgroup_id cgroup_name
	config_get tproxy_fw_mark "routing" "tproxy_fw_mark" "0x80"
	config_get tun_fw_mark "routing" "tun_fw_mark" "0x81"
	config_get tproxy_rule_pref "routing" "tproxy_rule_pref" "1024"
	config_get tun_rule_pref "routing" "tun_rule_pref" "1025"
	config_get tproxy_route_table "routing" "tproxy_route_table" "80"
	config_get tun_route_table "routing" "tun_route_table" "81"
	config_get cgroup_id "routing" "cgroup_id" "0x12061206"
	config_get cgroup_name "routing" "cgroup_name" "nikki"
	# prepare config
	local tproxy_enable; tproxy_enable=0
	if [ "$tcp_mode" = "tproxy" ] || [ "$udp_mode" = "tproxy" ]; then
		tproxy_enable=1
	fi
	local tun_enable; tun_enable=0
	if [ "$tcp_mode" = "tun" ] || [ "$udp_mode" = "tun" ]; then
		tun_enable=1
	fi
	if [ "$core_only" = 0 ]; then
		# proxy
		log "Proxy" "Enabled."
		# wait for tun device online
		if [ "$tun_enable" = 1 ]; then
			log "Proxy" "Waiting for tun device online within $tun_timeout seconds..."
			while [ "$tun_timeout" -gt 0 ]; do
				if (ip link show dev "$tun_device" > /dev/null 2>&1); then
					if [ "$(ip -json addr show dev "$tun_device" | tun_device="$tun_device" yq -M '.[] | select(.ifname = strenv(tun_device)) | .addr_info | length')" -gt 0 ]; then
						log "Proxy" "TUN device is online."
						break
					fi
				fi
				tun_timeout=$((tun_timeout - tun_interval))
				sleep "$tun_interval"
			done
			if [ "$tun_timeout" -le 0 ]; then
				log "Proxy" "Timeout, TUN device is not online."
				log "App" "Exit."
				return
			fi
		fi
		# fix compatible with dockerd
		## cgroupfs-mount
		### when cgroupfs-mount is installed, cgroupv1 will mounted instead of cgroupv2, we need to create cgroup manually
		if (mount | grep -q -w "^cgroup"); then
			mkdir -p "/sys/fs/cgroup/net_cls/$cgroup_name"
			echo "$cgroup_id" > "/sys/fs/cgroup/net_cls/$cgroup_name/net_cls.classid"
			cat "$PID_FILE_PATH" > "/sys/fs/cgroup/net_cls/$cgroup_name/cgroup.procs"
		fi
		## kmod-br-netfilter
		### when kmod-br-netfilter is loaded, bridge-nf-call-iptables and bridge-nf-call-ip6tables are set to 1, we need to set them to 0 if tproxy is enabled
		if [ "$tproxy_enable" = 1 ] && (lsmod | grep -q br_netfilter); then
			if [ "$ipv4_proxy" = 1 ]; then
				local bridge_nf_call_iptables; bridge_nf_call_iptables=$(sysctl -e -n net.bridge.bridge-nf-call-iptables)
				if [ "$bridge_nf_call_iptables" = 1 ]; then
					touch "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH"
					sysctl -q -w net.bridge.bridge-nf-call-iptables=0
				fi
			fi
			if [ "$ipv6_proxy" = 1 ]; then
				local bridge_nf_call_ip6tables; bridge_nf_call_ip6tables=$(sysctl -e -n net.bridge.bridge-nf-call-ip6tables)
				if [ "$bridge_nf_call_ip6tables" = 1 ]; then
					touch "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH"
					sysctl -q -w net.bridge.bridge-nf-call-ip6tables=0
				fi
			fi
		fi
		# ip route and rule
		if [ "$tproxy_enable" = 1 ]; then
			if [ "$ipv4_proxy" = 1 ]; then
				ip -4 route add local default dev lo table "$tproxy_route_table"
				ip -4 rule add pref "$tproxy_rule_pref" fwmark "$tproxy_fw_mark" table "$tproxy_route_table"
			fi
			if [ "$ipv6_proxy" = 1 ]; then
				ip -6 route add local default dev lo table "$tproxy_route_table"
				ip -6 rule add pref "$tproxy_rule_pref" fwmark "$tproxy_fw_mark" table "$tproxy_route_table"
			fi
		fi
		if [ "$tun_enable" = 1 ]; then
			if [ "$ipv4_proxy" = 1 ]; then
				ip -4 route add unicast default dev "$tun_device" table "$tun_route_table"
				ip -4 rule add pref "$tun_rule_pref" fwmark "$tun_fw_mark" table "$tun_route_table"
			fi
			if [ "$ipv6_proxy" = 1 ]; then
				ip -6 route add unicast default dev "$tun_device" table "$tun_route_table"
				ip -6 rule add pref "$tun_rule_pref" fwmark "$tun_fw_mark" table "$tun_route_table"
			fi
			$FIREWALL_INCLUDE_SH
		fi
		# hijack
		utpl -S "$HIJACK_UT" | nft -f -
		# check hijack
		if (nft list tables | grep -q nikki); then
			log "Proxy" "Hijack successful."
		else
			log "Proxy" "Hijack failed."
			log "App" "Exit."
		fi
	fi
}

service_stopped() {
	cleanup
}

reload_service() {
	cleanup
	start
}

service_triggers() {
	procd_add_reload_trigger "nikki"
}

cleanup() {
	# clear log
	clear_log
	# load config
	config_load nikki
	# get config
	## routing config
	local tproxy_route_table tun_route_table
	config_get tproxy_route_table "routing" "tproxy_route_table" "80"
	config_get tun_route_table "routing" "tun_route_table" "81"
	# delete routing policy
	ip -4 rule del table "$tproxy_route_table" > /dev/null 2>&1
	ip -4 rule del table "$tun_route_table" > /dev/null 2>&1
	ip -6 rule del table "$tproxy_route_table" > /dev/null 2>&1
	ip -6 rule del table "$tun_route_table" > /dev/null 2>&1
	# delete routing table
	ip -4 route flush table "$tproxy_route_table" > /dev/null 2>&1
	ip -4 route flush table "$tun_route_table" > /dev/null 2>&1
	ip -6 route flush table "$tproxy_route_table" > /dev/null 2>&1
	ip -6 route flush table "$tun_route_table" > /dev/null 2>&1
	# delete hijack
	nft delete table inet nikki > /dev/null 2>&1
	local handles handle
	handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "input" and .comment == "nikki") | .handle')
	for handle in $handles; do
		nft delete rule inet fw4 input handle "$handle"
	done
	handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "forward" and .comment == "nikki") | .handle')
	for handle in $handles; do
		nft delete rule inet fw4 forward handle "$handle"
	done
	# delete started flag
	rm "$STARTED_FLAG_PATH" > /dev/null 2>&1
	# revert fix compatible with dockerd
	## kmod-br-netfilter
	if (rm "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH" > /dev/null 2>&1); then
		sysctl -q -w net.bridge.bridge-nf-call-iptables=1
	fi
	if (rm "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH" > /dev/null 2>&1); then
		sysctl -q -w net.bridge.bridge-nf-call-ip6tables=1
	fi
	# delete cron
	sed -i "/#nikki/d" "/etc/crontabs/root" > /dev/null 2>&1
	/etc/init.d/cron restart
}

update_subscription() {
	local subscription_section; subscription_section="$1"
	if [ -z "$subscription_section" ]; then
		return
	fi
	# load config
	config_load nikki
	# get subscription config
	local subscription_name subscription_url subscription_user_agent
	config_get subscription_name "$subscription_section" "name"
	config_get subscription_url "$subscription_section" "url"
	config_get subscription_user_agent "$subscription_section" "user_agent"
	# reset subscription info
	uci_remove "nikki" "$subscription_section" "expire" > /dev/null 2>&1
	uci_remove "nikki" "$subscription_section" "upload" > /dev/null 2>&1
	uci_remove "nikki" "$subscription_section" "download" > /dev/null 2>&1
	uci_remove "nikki" "$subscription_section" "total" > /dev/null 2>&1
	uci_remove "nikki" "$subscription_section" "used" > /dev/null 2>&1
	uci_remove "nikki" "$subscription_section" "avaliable" > /dev/null 2>&1
	uci_remove "nikki" "$subscription_section" "update" > /dev/null 2>&1
	uci_remove "nikki" "$subscription_section" "success" > /dev/null 2>&1
	# update subscription
	log "Profile" "Update subscription: $subscription_name."
	local success
	local subscription_header_tmpfile; subscription_header_tmpfile="$TEMP_DIR/$subscription_section.header"
	local subscription_tmpfile; subscription_tmpfile="$TEMP_DIR/$subscription_section.yaml"
	local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml"
	if (curl -s -f -m 120 --connect-timeout 15 --retry 3 -L -X GET -A "$subscription_user_agent" -D "$subscription_header_tmpfile" -o "$subscription_tmpfile" "$subscription_url"); then
		log "Profile" "Subscription download successful."
		if (yq -p yaml -o yaml -e 'has("proxies") or has("proxy-providers")' "$subscription_tmpfile" > /dev/null 2>&1); then
			log "Profile" "Subscription is valid."
			success=1
		else
			log "Profile" "Subscription is not valid."
			success=0
		fi
	else
		log "Profile" "Subscription download failed."
		success=0
	fi
	# check if success
	if [ "$success" = 1 ]; then
		log "Profile" "Subscription update successful."
		local subscription_expire subscription_upload subscription_download subscription_total subscription_used subscription_avaliable
		subscription_expire=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2)
		subscription_upload=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2)
		subscription_download=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "download=[[:digit:]]+" | cut -d '=' -f 2)
		subscription_total=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "total=[[:digit:]]+" | cut -d '=' -f 2)
		if [ -n "$subscription_upload" ] && [ -n "$subscription_download" ]; then
			subscription_used=$((subscription_upload + subscription_download))
			if [ -n "$subscription_total" ]; then
				subscription_avaliable=$((subscription_total - subscription_used))
			fi
		fi
		# update subscription info
		if [ -n "$subscription_expire" ]; then
			uci_set "nikki" "$subscription_section" "expire" "$(date "+%Y-%m-%d %H:%M:%S" -d "@$subscription_expire")"
		fi
		if [ -n "$subscription_upload" ]; then
			uci_set "nikki" "$subscription_section" "upload" "$(format_filesize "$subscription_upload")"
		fi
		if [ -n "$subscription_download" ]; then
			uci_set "nikki" "$subscription_section" "download" "$(format_filesize "$subscription_download")"
		fi
		if [ -n "$subscription_total" ]; then
			uci_set "nikki" "$subscription_section" "total" "$(format_filesize "$subscription_total")"
		fi
		if [ -n "$subscription_used" ]; then
			uci_set "nikki" "$subscription_section" "used" "$(format_filesize "$subscription_used")"
		fi
		if [ -n "$subscription_avaliable" ]; then
			uci_set "nikki" "$subscription_section" "avaliable" "$(format_filesize "$subscription_avaliable")"
		fi
		uci_set "nikki" "$subscription_section" "update" "$(date "+%Y-%m-%d %H:%M:%S")"
		uci_set "nikki" "$subscription_section" "success" "1"
		# update subscription file
		rm -f "$subscription_header_tmpfile"
		mv -f "$subscription_tmpfile" "$subscription_file"
	elif [ "$success" = 0 ]; then
		log "Profile" "Subscription update failed."
		# update subscription info
		uci_set "nikki" "$subscription_section" "success" "0"
		# remove tmpfile
		rm -f "$subscription_header_tmpfile"
		rm -f "$subscription_tmpfile"
	fi
	uci_commit "nikki"
}
