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

. "${IPKG_INSTROOT}/lib/functions/network.sh"

USE_PROCD=1

START=99
STOP=10

CONF="fchomo"
PROG="/usr/bin/mihomo"

HM_DIR="/etc/fchomo"
TEMPS_DIR="$HM_DIR/templates"
EXE_DIR="/usr/libexec/fchomo"
SDL_DIR="/usr/share/fchomo"
RUN_DIR="/var/run/fchomo"
LOG_PATH="$RUN_DIR/fchomo.log"

# Compatibility
[ -x "$(command -v apk)" ] && OPM='apk' || OPM='opkg' # @less_25_12
#
# thanks to homeproxy
# we don't know which is the default server, just take the first one
DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
if [ -f "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG" ]; then
	DNSMASQ_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG")/dnsmasq-fchomo.d"
else
	DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-fchomo.d"
fi
#
# opmc action $@
opmc() {  # @less_25_12
	local action="$1"; shift;

	if [ "$OPM" = "apk" ]; then
	case "$action" in
		"list")
			action="list -q";;
		"list-installed")
			action="list -qI";;
	esac
	fi

	$OPM $action "$@"
}

config_load "$CONF"

# define global var: DEF_WAN DEF_WAN6 NIC_* NIC6_*
define_nic() {
	local dev sub addr
	# get all active NICs
	for dev in $(ls /sys/class/net/); do
		#ipv4
		sub=$(ip -o -4 addr|sed -En "s|.*${dev}\s+inet\s+([0-9\./]+).*|\1|gp")
		eval "NIC_${dev//[[:punct:]]/_}=\"\$sub\""
		#ipv6
		sub=$(ip -o -6 addr|sed -En "s|.*${dev}\s+inet6\s+([A-Za-z0-9\./:]+).*|\1|gp")
		# ref: https://github.com/openwrt/openwrt/blob/main/package/base-files/files/lib/functions/network.sh#L53 #network_get_subnet6()
		for _ in $sub; do
			for addr in $sub; do
				case "$addr" in fe[8ab]?:*|f[cd]??:*)
					continue
				esac
				sub=$addr; break
			done
			# Attempt to return first non-fe80::/10 range
			for addr in $sub; do
				case "$addr" in fe[8ab]?:*)
					continue
				esac
				sub=$addr; break
			done
			# Return first item
			for addr in $sub; do
				sub=$addr; break
			done
		done
		eval "NIC6_${dev//[[:punct:]]/_}=\"\$sub\""
	done
	# get default gateway 0.0.0.0/::
	network_find_wan DEF_WAN true
	network_find_wan6 DEF_WAN6 true

	return 0
}
define_nic

load_interfaces() {
	local bind_ifname
	config_get bind_ifname "$1" "bind_interface"

	[ -z "$bind_ifname" ] || interfaces=" $(uci -q show network|grep "device='$bind_ifname'"|cut -f2 -d'.') $interfaces"
}

log() {
	echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
}

start_service() {
	local client_enabled server_enabled
	config_get client_enabled "routing" "client_enabled" "0"
	config_get_bool server_enabled "routing" "server_enabled" "0"

	if [ "$client_enabled" = "0" -a "$server_enabled" = "0" ]; then
		return 1
	fi

	mkdir -p "$RUN_DIR"

	# Global ENV variables
	config_get_bool SKIP_SAFE_PATH_CHECK "experimental" "skip_safe_path_check" "0"
	export SKIP_SAFE_PATH_CHECK=$([ "$SKIP_SAFE_PATH_CHECK" = "1" ] && echo true || echo false)

	# Client
	if [ "$client_enabled" = "1" ]; then
	if [ -z "$1" -o "$1" = "mihomo-c" ]; then

		# Generate/Validate client config
		ucode -S "$SDL_DIR/generate_client.uc" 2>>"$LOG_PATH" | yq -Poy | yq \
			'.sniffer["force-domain"][] style="double"
			| .sniffer["skip-domain"][] style="double"
			| with(.dns["nameserver-policy"] | keys; .. style="double")
			| .dns["fallback-filter"].domain[] style="double"
			| with(.["proxy-providers"][] | select(.payload); .payload style="literal")
			| with(.["rule-providers"][] | select(.payload); .payload style="literal")' \
			| sed -E 's,^(\s*payload:) \|-,\1,' \
		> "$RUN_DIR/mihomo-c.yaml"
		yq eval-all -i '. as $item ireduce ({}; . * $item )' "$RUN_DIR/mihomo-c.yaml" "$TEMPS_DIR/"*.yaml

		if [ ! -e "$RUN_DIR/mihomo-c.yaml" ]; then
			log "Error: failed to generate client configuration."
			return 1
		else
			# Set ENV variables for Client
			export SAFE_PATHS="$RUN_DIR$(
				yq 'with(.external-ui; select(. != null) | . = sub("(/?$)", "/placeholder"))
					| [.external-ui, .tls.certificate, .tls.private-key, .tls.client-auth-cert] | unique | map(select(. != null))
					| .[] |= sub("(/[^/]+$)", "") | unique
					| .[] | sub("(^)", ":")' \
				"$RUN_DIR/mihomo-c.yaml" | tr -d '\n'
			)"
			export SAFE_PATHS="$SAFE_PATHS$( # mTLS
				yq '.proxies | map([.certificate, .private-key][]) | unique | map(select(. != null))
					| .[] |= sub("(/[^/]+$)", "") | unique
					| .[] | sub("(^)", ":")' \
				"$RUN_DIR/mihomo-c.yaml" | tr -d '\n'
			)"

			if ! "$PROG" -t -d "$HM_DIR" -f "$RUN_DIR/mihomo-c.yaml" >/dev/null; then
				log "Error: wrong client configuration detected."
				"$PROG" -t -d "$HM_DIR" -f "$RUN_DIR/mihomo-c.yaml" >>"$LOG_PATH"
				return 1
			fi
		fi
		echo > "$RUN_DIR/mihomo-c.log"

		# Deploy Clash API Dashboard
		local dashboard_repo
		config_get dashboard_repo "api" "dashboard_repo" ""

		if [ -n "$dashboard_repo" -a ! -d "$RUN_DIR/ui" ]; then
			tar -xzf "$HM_DIR/resources/$(echo "$dashboard_repo" | sed 's|\W|_|g').tgz" -C "$RUN_DIR/"
			mv "$RUN_DIR/"*-gh-pages/ "$RUN_DIR/ui/"
		fi

		# Generate runtime ruleset
		# <format> <src> <dst> [domainOnlay]
		write_runtime_ruleset_file() {
			local format="$1"
			local src="$2"
			local dst="$3"
			local domainOnlay="$4"

			if [ "$format" = "fchmclist" ]; then
				if [ -n "$domainOnlay" ]; then
					yq '.[] |= with(select(. == null); . = [])
						| with(.DOMAIN[]; . = "DOMAIN-SUFFIX,\(.)")
					| {"payload": .DOMAIN}' "$src" > "$dst"
				else
					yq '.[] |= with(select(. == null); . = [])
						| with(.DOMAIN[]; . = "DOMAIN-SUFFIX,\(.)")
						| with(.IPCIDR[]; . = "IP-CIDR,\(.)")
						| with(.IPCIDR6[]; . = "IP-CIDR6,\(.)")
					| {"payload": [.DOMAIN[], .IPCIDR[], .IPCIDR6[]]}' "$src" > "$dst"
				fi
			elif [ "$format" = "domain" ]; then
				sed -E 's|^([^\.])|+.\1|' "$src" > "$dst.tmp"
				mihomo convert-ruleset domain text "$dst.tmp" "$dst"
				rm -f "$dst.tmp"
			fi
		}

		# runtime ruleset
		write_runtime_ruleset_file fchmclist "$HM_DIR/resources/direct_list.yaml" "$RUN_DIR/direct_list.yaml" domainOnlay
		write_runtime_ruleset_file fchmclist "$HM_DIR/resources/proxy_list.yaml" "$RUN_DIR/proxy_list.yaml"
		write_runtime_ruleset_file domain "$HM_DIR/resources/china_list.txt" "$RUN_DIR/china_list.mrs"
		write_runtime_ruleset_file domain "$HM_DIR/resources/gfw_list.txt" "$RUN_DIR/gfw_list.mrs"

		# Setup DNSMasq servers and IP-sets
		local global_ipv6 dns_ipv6
		config_get_bool global_ipv6 "global" "ipv6" "1"
		config_get_bool dns_ipv6 "dns" "ipv6" "1"
		local dns_port tunnel_port
		config_get dns_port "dns" "dns_port" "7853"
		config_get tunnel_port "inbound" "tunnel_port" "7893" # @Not required for v1.19.2+
		local top_upstream routing_mode routing_domain
		config_get_bool top_upstream "routing" "top_upstream" "0"
		config_get routing_mode "routing" "routing_mode" ""
		config_get_bool routing_domain "routing" "routing_domain" "0"

		mkdir -p "$DNSMASQ_DIR"
		echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../$([ "$top_upstream" = "1" ] && echo '00-')dnsmasq-fchomo.conf"
		cat <<-EOF > "$DNSMASQ_DIR/forward-dns.conf"
			no-poll
			no-resolv
			server=127.0.0.1#$dns_port
		EOF

		# fix dnsmasq
		if [ "$top_upstream" = "1" -a "$(uci -q get dhcp.@dnsmasq[0].strictorder)" != "1" ]; then
			uci set dhcp.@dnsmasq[0].strictorder="1"
			uci commit dhcp
		fi

		# <family> <set_name> <src> <dst> [yaml]
		write_ipset_file() {
			local family=$1
			local set_name=$2
			local src="$3"
			local dst="$4"
			local yaml="$5"

			if [ -n "$yaml" ]; then
				yq '.[] |= with(select(. == null); . = []) | .DOMAIN[]' "$src" | \
				sed "s|^|nftset=/|;s|$|/${family}#inet#fchomo#${set_name}|" > "$dst"
			else
				sed "s|^|nftset=/|;s|$|/${family}#inet#fchomo#${set_name}|" "$src" > "$dst"
			fi
		}

		# IP-sets
		if [ -n "$(opmc list-installed dnsmasq-full)" ]; then
			write_ipset_file 4 inet4_wan_direct_addr "$HM_DIR/resources/direct_list.yaml" "$DNSMASQ_DIR/direct_list.conf" yaml
			[ "$global_ipv6" != "1" ] || \
			write_ipset_file 6 inet6_wan_direct_addr "$HM_DIR/resources/direct_list.yaml" "$DNSMASQ_DIR/direct_list6.conf" yaml

			write_ipset_file 4 inet4_wan_proxy_addr "$HM_DIR/resources/proxy_list.yaml" "$DNSMASQ_DIR/proxy_list.conf" yaml
			[ "$global_ipv6" != "1" ] || \
			write_ipset_file 6 inet6_wan_proxy_addr "$HM_DIR/resources/proxy_list.yaml" "$DNSMASQ_DIR/proxy_list6.conf" yaml

			if [ "$routing_domain" = "1" ]; then
				case "$routing_mode" in
					bypass_cn)
						write_ipset_file 4 inet4_china_list_addr "$HM_DIR/resources/china_list.txt" "$DNSMASQ_DIR/china_list.conf"
						[ "$global_ipv6" != "1" ] || \
						write_ipset_file 6 inet6_china_list_addr "$HM_DIR/resources/china_list.txt" "$DNSMASQ_DIR/china_list6.conf"
						;;
					routing_gfw)
						write_ipset_file 4 inet4_gfw_list_addr "$HM_DIR/resources/gfw_list.txt" "$DNSMASQ_DIR/gfw_list.conf"
						[ "$global_ipv6" != "1" ] || \
						write_ipset_file 6 inet6_gfw_list_addr "$HM_DIR/resources/gfw_list.txt" "$DNSMASQ_DIR/gfw_list6.conf"
						;;
				esac
			fi
		fi

		# Setup routing table
		local proxy_mode table_id rule_pref
		config_get proxy_mode "inbound" "proxy_mode" "redir_tproxy"
		config_get table_id "config" "route_table_id" "2022"
		config_get rule_pref "config" "route_rule_pref" "9000"
		case "$proxy_mode" in
			"redir_tproxy")
				local tproxy_mark
				config_get tproxy_mark "config" "tproxy_mark" "201"

				ip rule add fwmark "$tproxy_mark" pref "$rule_pref" table "$table_id"
				ip route add local default dev lo table "$table_id"

				if [ "$global_ipv6" = "1" ]; then
					ip -6 rule add fwmark "$tproxy_mark" pref "$rule_pref" table "$table_id"
					ip -6 route add local default dev lo table "$table_id"
				fi
				;;
			"redir_tun"|"tun")
				local tun_name tun_mark
				config_get tun_name "config" "tun_name" "hmtun0"
				config_get tun_mark "config" "tun_mark" "202"

				ip tuntap add mode tun user root name "$tun_name"
				sleep 1s
				ip link set "$tun_name" up

				ip route replace default dev "$tun_name" table "$table_id"
				ip rule add fwmark "$tun_mark" pref "$rule_pref" table "$table_id"

				if [ "$global_ipv6" = "1" ]; then
					ip -6 route replace default dev "$tun_name" table "$table_id"
					ip -6 rule add fwmark "$tun_mark" pref "$rule_pref" table "$table_id"
				fi
				;;
		esac

		# mihomo (client)
		procd_open_instance "mihomo-c"

		procd_set_param command /bin/sh
		procd_append_param command -c "'$PROG' -d '$HM_DIR' -f '$RUN_DIR/mihomo-c.yaml' >> '$RUN_DIR/mihomo-c.log' 2>&1"
		procd_set_param env SAFE_PATHS="$SAFE_PATHS" SKIP_SAFE_PATH_CHECK="$SKIP_SAFE_PATH_CHECK" # The syntax of this environment variable is the same as the PATH environment variable parsing rules of this operating system (i.e., semicolon-separated under Windows and colon-separated under other systems)

		# Only supports `Global`` and does not support `Proxy Group` and `Proxy Node`
		local bind_ifname
		config_get bind_ifname "routing" "bind_interface"

		procd_set_param netdev "br-lan"
		if [ -n "$bind_ifname" ]; then
			procd_append_param netdev "$bind_ifname"
		else
			local ifname
			network_get_device ifname "$DEF_WAN" && procd_append_param netdev "$ifname"
			network_get_device ifname "$DEF_WAN6" && procd_append_param netdev "$ifname"
		fi

		#procd_set_param capabilities "/etc/capabilities/fchomo.json"
		#procd_set_param user mihomo
		#procd_set_param group mihomo

		procd_set_param limits core="unlimited"
		procd_set_param limits nofile="1000000 1000000"
		procd_set_param stderr 1
		procd_set_param respawn

		procd_close_instance
	fi
	fi

	# Server
	if [ "$server_enabled" = "1" ]; then
	if [ -z "$1" -o "$1" = "mihomo-s" ]; then
		# Generate/Validate server config
		ucode -S "$SDL_DIR/generate_server.uc" 2>>"$LOG_PATH" | yq -Poy > "$RUN_DIR/mihomo-s.yaml"

		if [ ! -e "$RUN_DIR/mihomo-s.yaml" ]; then
			log "Error: failed to generate server configuration."
			return 1
		else
			# Set ENV variables for Server
			export SAFE_PATHS="$RUN_DIR$(
				yq '.listeners | map([.certificate, .private-key, .client-auth-cert][]) | unique | map(select(. != null))
					| .[] |= sub("(/[^/]+$)", "") | unique
					| .[] | sub("(^)", ":")' \
				"$RUN_DIR/mihomo-s.yaml" | tr -d '\n'
			)"

			if ! "$PROG" -t -d "$HM_DIR" -f "$RUN_DIR/mihomo-s.yaml" >/dev/null; then
				log "Error: wrong server configuration detected."
				"$PROG" -t -d "$HM_DIR" -f "$RUN_DIR/mihomo-s.yaml" >>"$LOG_PATH"
				return 1
			fi
		fi
		echo > "$RUN_DIR/mihomo-s.log"

		# mihomo (server)
		procd_open_instance "mihomo-s"

		procd_set_param command /bin/sh
		procd_append_param command -c "'$PROG' -d '$HM_DIR' -f '$RUN_DIR/mihomo-s.yaml' >> '$RUN_DIR/mihomo-s.log' 2>&1"
		procd_set_param env SAFE_PATHS="$SAFE_PATHS" SKIP_SAFE_PATH_CHECK="$SKIP_SAFE_PATH_CHECK" # The syntax of this environment variable is the same as the PATH environment variable parsing rules of this operating system (i.e., semicolon-separated under Windows and colon-separated under other systems)

		#procd_set_param capabilities "/etc/capabilities/fchomo.json"
		#procd_set_param user mihomo
		#procd_set_param group mihomo

		procd_set_param limits core="unlimited"
		procd_set_param limits nofile="1000000 1000000"
		procd_set_param stderr 1
		procd_set_param respawn

		# add_firewall
		add_firewall() {
			local enabled auto_firewall listen port
			config_get_bool enabled "$1" "enabled" "1"
			config_get_bool auto_firewall "$1" "auto_firewall" "1"
			config_get listen "$1" "listen" "::"
			config_get port "$1" "port"

			[ "$enabled" = "0" ] && return 0
			[ "$auto_firewall" = "0" ] && return 0

			json_add_object ''
			json_add_string type rule
			json_add_string target ACCEPT
			json_add_string name "$1"
			#json_add_string family '' # '' = IPv4 and IPv6
			json_add_string proto 'tcp udp'
			json_add_string direction in
			json_add_string src "*"
			#json_add_string dest '' # '' = input
			json_add_string dest_ip "$(echo "$listen" | grep -vE '^(0\.\d+\.\d+\.\d+|::)$')"
			json_add_string dest_port "$port"
			json_close_object
		}
		#
		procd_open_data
		# configure firewall
		json_add_array firewall
			# meta l4proto %s th dport %s counter accept comment "!%s: accept server instance [%s]"
			config_foreach add_firewall "server"
		json_close_array
		procd_close_data

		procd_close_instance
	fi
	fi

	# log-cleaner
	procd_open_instance "log-cleaner"
	procd_set_param command "$EXE_DIR/clean_log.sh"
	procd_set_param respawn
	procd_close_instance

	# Setup firewall
	utpl -S "$SDL_DIR/firewall_pre.ut" > "$RUN_DIR/fchomo_pre.nft"
	# Setup Nftables rules
	if [ "$client_enabled" = "1" ]; then
		[ -z "$1" -o "$1" = "mihomo-c" ] && utpl -S "$SDL_DIR/firewall_post.ut" > "$RUN_DIR/fchomo_post.nft"
	fi

	log "$(mihomo -v | awk 'NR==1{print $1,$3}') started."
}

service_started() {
	procd_set_config_changed dnsmasq
	procd_set_config_changed firewall
}

stop_service() {
	# Client
	[ -z "$1" -o "$1" = "mihomo-c" ] && stop_client
	# Server
	[ -z "$1" -o "$1" = "mihomo-s" ] && stop_server
	# Setup firewall
	echo 2>"/dev/null" > "$RUN_DIR/fchomo_pre.nft"
	return 0
}

stop_client() {
	# Load config
	local table_id tproxy_mark tun_mark tun_name
	config_get table_id "cofnig" "route_table_id" "2022"
	config_get rule_pref "config" "route_rule_pref" "9000"
	config_get tproxy_mark "cofnig" "tproxy_mark" "201"
	config_get tun_mark "cofnig" "tun_mark" "202"
	config_get tun_name "cofnig" "tun_name" "hmtun0"

	# Remove routing table
	# Tproxy
	ip rule del pref "$rule_pref" table "$table_id" 2>"/dev/null"
	ip route del local default dev lo table "$table_id" 2>"/dev/null"

	ip -6 rule del pref "$rule_pref" table "$table_id" 2>"/dev/null"
	ip -6 route del local default dev lo table "$table_id" 2>"/dev/null"

	# TUN
	ip route del default dev "$tun_name" table "$table_id" 2>"/dev/null"
	ip rule del pref "$rule_pref" table "$table_id" 2>"/dev/null"

	ip -6 route del default dev "$tun_name" table "$table_id" 2>"/dev/null"
	ip -6 rule del pref "$rule_pref" table "$table_id" 2>"/dev/null"

	# Remove Nftables rules
	nft flush  table inet fchomo 2>"/dev/null"
	nft delete table inet fchomo 2>"/dev/null"
	echo 2>"/dev/null" > "$RUN_DIR/fchomo_post.nft"

	# Remove runtime ruleset
	echo 2>"/dev/null" > "$RUN_DIR/direct_list.yaml"
	echo 2>"/dev/null" > "$RUN_DIR/proxy_list.yaml"
	rm -rf "$RUN_DIR/china_list.mrs" "$RUN_DIR/gfw_list.mrs"

	# Remove DNSMasq servers
	rm -rf "$DNSMASQ_DIR/../dnsmasq-fchomo.conf" "$DNSMASQ_DIR/../00-dnsmasq-fchomo.conf" "$DNSMASQ_DIR" # no bash, no {,00-}dnsmasq-fchomo.conf

	# Remove Clash API Dashboard
	rm -rf "$RUN_DIR/ui"

	# Remove client config
	rm -f "$RUN_DIR/mihomo-c.yaml" "$RUN_DIR/mihomo-c.log"

	log "Service mihomo-c stopped."
}

stop_server() {
	# Remove server config
	rm -f "$RUN_DIR/mihomo-s.yaml" "$RUN_DIR/mihomo-s.log"

	log "Service mihomo-s stopped."
}

service_stopped() {
	sleep 1s # Wait for procd_kill complete
	# Client
	[ -n "$(/etc/init.d/$CONF info | jsonfilter -q -e '@.'"$CONF"'.instances["mihomo-c"]')" ] || client_stopped
	# Server

	procd_set_config_changed dnsmasq
	procd_set_config_changed firewall
}

client_stopped() {
	# Load config
	local tun_name
	config_get tun_name "config" "tun_name" "hmtun0"

	# TUN
	ip link set "$tun_name" down 2>"/dev/null"
	ip tuntap del mode tun name "$tun_name" 2>"/dev/null"

	ip -6 rule del oif "$tun_name" 2>"/dev/null"
}

server_stopped() {
	return 0
}

reload_service() {
	log "Reloading service${1:+ $1}..."

	stop  "$@"
	start "$@"
}

service_triggers() {
	procd_add_reload_trigger "$CONF" 'network'

	local interfaces

	# Only supports `Global`` and does not support `Proxy Group` and `Proxy Node`
	load_interfaces 'routing'
	[ -n "$interfaces" ] && {
		for n in $interfaces; do
			procd_add_reload_interface_trigger $n
		done
	} || {
		for n in $DEF_WAN $DEF_WAN6; do
			procd_add_reload_interface_trigger $n
		done
	}

	interfaces=$(uci show network|grep "device='br-lan'"|cut -f2 -d'.')
	[ -n "$interfaces" ] && {
		for n in $interfaces; do
			procd_add_reload_interface_trigger $n
		done
	}
}
