#!/bin/sh /etc/rc.common
USE_PROCD=1
EXTRA_COMMANDS="refresh_cron"

START=99
STOP=15

CORE_LOG_PATH="/var/log/clashoo/core.log"
SINGBOX_BIN_DIR="/usr/share/clashoo/bin"
SINGBOX_RUNTIME_DIR="/var/run/clashoo/sing-box"
SINGBOX_RUNTIME_CONFIG="$SINGBOX_RUNTIME_DIR/config.json"
SINGBOX_WORKDIR="$SINGBOX_RUNTIME_DIR/work"

. /usr/share/clashoo/runtime/dns_helpers.sh 2>/dev/null || true

command_exists() {
	command -v "$1" >/dev/null 2>&1
}

shell_quote() {
	printf "'"
	printf '%s' "$1" | sed "s/'/'\\\\''/g"
	printf "'"
}

use_fw4_backend() {
	command_exists fw4 && command_exists nft
}

backend_name() {
	if use_fw4_backend; then
		echo "fw4"
	else
		echo "unsupported"
	fi
}

backend_apply_rules() {
	if use_fw4_backend; then
		/usr/share/clashoo/net/fw4.sh apply
	else
		_log_runtime_error "Current environment requires fw4+nft support"
		return 1
	fi
}

backend_remove_rules() {
	if use_fw4_backend; then
		/usr/share/clashoo/net/fw4.sh remove
	else
		return 0
	fi
}

_clashoo_enabled() {
	[ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]
}

# Compute start-wait window: TUN needs longer for utun setup + fake-ip pool init.
# Override via `uci set clashoo.config.start_wait_secs=N` for slow devices.
_compute_start_wait() {
	local override
	override="$(uci -q get clashoo.config.start_wait_secs)"
	if [ -n "$override" ] && [ "$override" -gt 0 ] 2>/dev/null; then
		echo "$override"; return
	fi
	if [ "$(uci -q get clashoo.config.tun_mode)" = "1" ]; then
		echo 15
	else
		echo 6
	fi
}

selected_singbox_binary() {
	local dcore
	dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)"
	case "$dcore" in
		4) echo "$SINGBOX_BIN_DIR/sing-box-stable" ;;
		5) echo "$SINGBOX_BIN_DIR/sing-box-alpha" ;;
		*) return 1 ;;
	esac
}

_migrate_legacy_singbox_binary() {
	local dcore target legacy staged
	dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)"
	target="$(selected_singbox_binary)" || return 1
	[ -e "$target" ] && return 0
	case "$dcore" in
		4) legacy="/usr/bin/sing-box-stable" ;;
		5) legacy="/usr/bin/sing-box-alpha" ;;
		*) return 1 ;;
	esac
	[ -x "$legacy" ] || return 1
	mkdir -p "$SINGBOX_BIN_DIR" >/dev/null 2>&1 || return 1
	staged="${target}.$$"
	rm -f "$staged" >/dev/null 2>&1
	cp -f "$legacy" "$staged" >/dev/null 2>&1 || {
		rm -f "$staged" >/dev/null 2>&1
		return 1
	}
	chmod 0755 "$staged" >/dev/null 2>&1 || {
		rm -f "$staged" >/dev/null 2>&1
		return 1
	}
	mv -f "$staged" "$target" >/dev/null 2>&1 || {
		rm -f "$staged" >/dev/null 2>&1
		return 1
	}
	return 0
}

_ensure_singbox_binary() {
	local target
	target="$(selected_singbox_binary)" || return 1
	[ -e "$target" ] || _migrate_legacy_singbox_binary || return 1
	[ -x "$target" ]
}

_singbox_pid_owns_private_binary() {
	local pid stable alpha exe cmdline
	pid="$1"
	stable="$SINGBOX_BIN_DIR/sing-box-stable"
	alpha="$SINGBOX_BIN_DIR/sing-box-alpha"
	[ -d "/proc/$pid" ] || return 1
	if command_exists readlink; then
		exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null)"
		if [ "$exe" = "$stable" ] || [ "$exe" = "$alpha" ]; then
			return 0
		fi
	fi
	cmdline="$(tr '\000' ' ' < "/proc/$pid/cmdline" 2>/dev/null)"
	case "$cmdline" in
		"$stable"|"$stable "*|"$alpha"|"$alpha "*) return 0 ;;
	esac
	return 1
}

_singbox_instance_pid() {
	local _pid
	_pid="$(ubus call service list '{"name":"clashoo","verbose":true}' 2>/dev/null \
		| jsonfilter -e '@.clashoo.instances.singbox.pid' 2>/dev/null | head -n1)"
	[ -n "$_pid" ] && _singbox_pid_owns_private_binary "$_pid" && echo "$_pid"
}

_singbox_running() {
	local _pid
	_pid="$(_singbox_instance_pid)"
	[ -n "$_pid" ] && [ -d "/proc/$_pid" ]
}

# Roll back partial state when start failed after backend rules / DNS were applied.
# Reuses the same cleanup primitives as stop_service so behavior stays consistent.
_rollback_failed_start() {
	log_msg "Rolling back failed start" "启动失败，正在回退防火墙与 DNS 配置"
	backend_remove_rules >/dev/null 2>&1
		cleanup_stale_core_nft >/dev/null 2>&1
	revert_dns >/dev/null 2>&1
	stop_singbox_service >/dev/null 2>&1
	_kill_core >/dev/null 2>&1
	rm -rf /www/luci-static/yacd 2>/dev/null
	del_cron >/dev/null 2>&1
}

cleanup_stale_core_nft() {
	for _nft_table in "inet mihomo" "inet sing-box"; do
		nft delete table $_nft_table 2>/dev/null || true
	done
}

_core_candidates() {
	echo "/usr/share/clashoo/bin/mihomo-stable /usr/libexec/clashoo/mihomo-stable /usr/share/clashoo/bin/mihomo-alpha /usr/bin/mihomo /usr/bin/clash-meta /etc/clashoo/clash"
}

_migrate_legacy_mihomo_stable() {
	local legacy=/usr/bin/clash-meta target=/usr/share/clashoo/bin/mihomo-stable version staged
	[ -x "$target" ] && return 0
	[ -x "$legacy" ] && [ ! -L "$legacy" ] || return 0
	version=$("$legacy" -v 2>/dev/null | head -n 1)
	[ -n "$version" ] || return 0
	echo "$version" | grep -Eiq '(alpha|beta|rc|pre|smart)' && return 0
	mkdir -p "$(dirname "$target")" || return 1
	staged="${target}.$$"
	cp -f "$legacy" "$staged" 2>/dev/null && chmod 0755 "$staged" && mv -f "$staged" "$target"
}

_mihomo_stable_binary() {
	local bundled=/usr/libexec/clashoo/mihomo-stable manual=/usr/share/clashoo/bin/mihomo-stable bundled_ver manual_ver
	# A successful custom download is an explicit override, even if its version is older.
	if [ -x "$manual" ] &&
		[ "$(sed -n '1{s/^[[:space:]]*//;s/[[:space:]]*$//;p;}' /usr/share/clashoo/clash_meta_version 2>/dev/null)" = "custom-url" ]; then
		echo "$manual"
		return 0
	fi
	[ -x "$bundled" ] || { [ -x "$manual" ] && { echo "$manual"; return 0; }; return 1; }
	[ -x "$manual" ] || { echo "$bundled"; return; }
	bundled_ver="$("$bundled" -v 2>/dev/null | awk '{for (i = 1; i < NF; i++) if ($i == "Meta") {print $(i + 1); exit}}')"
	manual_ver="$("$manual" -v 2>/dev/null | awk '{for (i = 1; i < NF; i++) if ($i == "Meta") {print $(i + 1); exit}}')"
	bundled_ver="${bundled_ver#v}"
	manual_ver="${manual_ver#v}"
	printf '%s\n' "$bundled_ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || bundled_ver=""
	printf '%s\n' "$manual_ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || manual_ver=""
	if [ -n "$bundled_ver" ] && [ -n "$manual_ver" ]; then
		if awk -v manual="$manual_ver" -v bundled="$bundled_ver" 'BEGIN {
			split(manual, m, "."); split(bundled, b, ".");
			for (i = 1; i <= 3; i++) {
				if (m[i] + 0 > b[i] + 0) exit 0;
				if (m[i] + 0 < b[i] + 0) exit 1;
			}
			exit 0;
		}'; then
			echo "$manual"
		else
			echo "$bundled"
		fi
	elif [ -n "$manual_ver" ]; then
		echo "$manual"
	else
		echo "$bundled"
	fi
}

_detect_selected_core() {
	local selected
	selected=$(uci get clashoo.config.core 2>/dev/null)
	case "$selected" in
		1)
			for _bin in /usr/bin/smart /etc/clashoo/clash /usr/bin/clash; do
				[ -x "$_bin" ] && echo "$_bin" && return
			done
			return 1
			;;
		2|mihomo)
			_mihomo_stable_binary
			return $?
			;;
		3|mihomo-alpha)
			[ -x /usr/share/clashoo/bin/mihomo-alpha ] && echo /usr/share/clashoo/bin/mihomo-alpha && return
			return 1
			;;
	esac
	_detect_core
}

core_supports_advanced_features() {
	case "$CLASH" in
		*/mihomo|*/mihomo-stable|*/mihomo-alpha|*/clash-meta|*/smart)
			return 0
			;;
		*)
			return 1
			;;
	esac
}

# only trust procd instance PIDs (not binary name); openclash/nikki/passwall share
# the same binary names. Use ubus service list as source of truth.
_clashoo_instance_pid() {
	ubus call service list "{\"name\":\"clashoo\",\"verbose\":true}" 2>/dev/null \
		| jsonfilter -e '@.clashoo.instances.*.pid' 2>/dev/null | head -n1
}

_core_running() {
	local _pid
	_pid="$(_clashoo_instance_pid)"
	[ -n "$_pid" ] && [ -d "/proc/$_pid" ]
}

# kill only clashoo procd instances, never by binary name — would hit co-installed plugins.
_kill_core() {
	local _pid _wait
	_pid="$(_clashoo_instance_pid)"
	[ -n "$_pid" ] || return 0
	kill "$_pid" >/dev/null 2>&1
	_wait=0
	while [ "$_wait" -lt 3 ] && [ -d "/proc/$_pid" ]; do
		sleep 1
		_wait=$((_wait + 1))
	done
	[ -d "/proc/$_pid" ] && kill -9 "$_pid" >/dev/null 2>&1
}

_selected_core_running() {
	if is_singbox_selected; then
		_singbox_running
	else
		_core_running
	fi
}

selected_core_family() {
	local core_type
	core_type="$(uci -q get clashoo.config.core_type 2>/dev/null)"
	[ "$core_type" = "singbox" ] && echo "singbox" || echo "mihomo"
}

is_singbox_selected() {
	[ "$(selected_core_family)" = "singbox" ]
}

sync_legacy_core_field() {
	local dcore legacy
	dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)"
	if is_singbox_selected; then
		[ "$dcore" = "5" ] && legacy="5" || legacy="4"
	else
		case "$dcore" in
			1) legacy="1" ;;
			3) legacy="3" ;;
			*) legacy="2" ;;
		esac
	fi
	uci -q set clashoo.config.core="$legacy"
	uci -q commit clashoo >/dev/null 2>&1
}

prepare_singbox_runtime() {
	local active src redir_port tproxy_port mixed_port dns_port tmp_config
	active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)"
	src="/usr/share/clashoo/config/singbox/${active}"
	tmp_config="${SINGBOX_RUNTIME_CONFIG}.tmp.$$"

	mkdir -p "$SINGBOX_RUNTIME_DIR" "$SINGBOX_WORKDIR" "$(dirname "$CORE_LOG_PATH")" >/dev/null 2>&1 || return 1
	rm -f "$tmp_config" >/dev/null 2>&1
	if [ -n "$active" ] && [ -r "$src" ]; then
		cp -f "$src" "$tmp_config" >/dev/null 2>&1 || {
			rm -f "$tmp_config" >/dev/null 2>&1
			return 1
		}
	elif [ -r "$SINGBOX_RUNTIME_CONFIG" ]; then
		cp -f "$SINGBOX_RUNTIME_CONFIG" "$tmp_config" >/dev/null 2>&1 || {
			rm -f "$tmp_config" >/dev/null 2>&1
			return 1
		}
	else
		_log_runtime_error "sing-box config.json is missing; please generate or select a sing-box profile first"
		rm -f "$tmp_config" >/dev/null 2>&1
		return 1
	fi

	command -v ucode >/dev/null 2>&1 || {
		_log_runtime_error "ucode is required to prepare sing-box runtime config"
		rm -f "$tmp_config" >/dev/null 2>&1
		return 1
	}
	# Migrate every imported config, including core-only mode, before any
	# Clashoo-specific normalization.  The temp file is in the runtime dir so
	# a failed migration/normalization never replaces the last good config.
	ucode /usr/share/clashoo/lib/migrate_singbox.uc "$tmp_config" >/dev/null 2>&1 || {
		_log_runtime_error "sing-box runtime config migration failed"
		rm -f "$tmp_config" >/dev/null 2>&1
		return 1
	}

	if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then
		local has_tun_device dash_port dash_pass

		has_tun_device=1
		runtime_has_tun_device || has_tun_device=0
		redir_port="$(uci -q get clashoo.config.redir_port 2>/dev/null)"
		tproxy_port="$(uci -q get clashoo.config.tproxy_port 2>/dev/null)"
		mixed_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)"
		dns_port="$(uci -q get clashoo.config.listen_port 2>/dev/null)"
		dash_port="$(uci -q get clashoo.config.dash_port 2>/dev/null)"
		dash_pass="$(uci -q get clashoo.config.dash_pass 2>/dev/null)"
		[ -z "$redir_port" ] && redir_port=7891
		[ -z "$tproxy_port" ] && tproxy_port=7982
		[ -z "$mixed_port" ] && mixed_port=7890
		[ -z "$dns_port" ] && dns_port=1053
		[ -z "$dash_port" ] && dash_port=9090
		# routing_mark 6666 must match fw4.sh CORE_ROUTING_MARK (0x1a0a).
		# Never reuse PROXY_FWMARK (0x162) — it would pull egress to lo -> unreachable.
		ucode /usr/share/clashoo/lib/normalize_singbox_config.uc \
			"$tmp_config" "$redir_port" "$tproxy_port" "$mixed_port" "$has_tun_device" "6666" "$dns_port" "$dash_port" "$dash_pass" >/dev/null 2>&1 || {
			_log_runtime_error "sing-box runtime config normalization failed"
			rm -f "$tmp_config" >/dev/null 2>&1
			return 1
		}
	fi

	mv -f "$tmp_config" "$SINGBOX_RUNTIME_CONFIG" >/dev/null 2>&1 || {
		rm -f "$tmp_config" >/dev/null 2>&1
		return 1
	}
	return 0
}

start_singbox_service() {
	local bin
	bin="$(selected_singbox_binary)" || return 1
	if ! _ensure_singbox_binary; then
		_log_runtime_error "sing-box core not found, please install the local sing-box binary first"
		return 1
	fi
	prepare_singbox_runtime || return 1
	# preflight dry-run with watchdog: bail on bad config without touching
	# firewall/dnsmasq, and never let a hung check freeze startup/UI.
	_run_with_timeout 15 "$bin" -c "$SINGBOX_RUNTIME_CONFIG" check >> "$CORE_LOG_PATH" 2>&1
	_pf_rc=$?
	if [ "$_pf_rc" -ne 0 ]; then
		_pf_reason="$(_preflight_reason)"
		if [ "$_pf_rc" -eq 124 ]; then
			log_msg "sing-box config preflight timed out, retry shortly. ${_pf_reason}" "sing-box 配置预检超时，稍后自动重试。${_pf_reason}"
			runtime_state_record_health "fail" "preflight:singbox_timeout"
		else
			log_msg "sing-box config dry-run failed; firewall/DNS untouched. ${_pf_reason}" "sing-box 配置预检失败，未应用防火墙与 DNS 改动。${_pf_reason}"
			runtime_state_record_health "fail" "preflight:singbox_dry_run_failed"
		fi
		return 1
	fi
	procd_open_instance "singbox"
	procd_set_param command "$bin" run -c "$SINGBOX_RUNTIME_CONFIG" -D "$SINGBOX_WORKDIR"
	procd_set_param user root
	procd_set_param file "$SINGBOX_RUNTIME_CONFIG"
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_set_param limits core="unlimited"
	procd_set_param limits nofile="1000000 1000000"
	procd_set_param respawn 3600 5 5
	procd_close_instance
}

stop_singbox_service() {
	local _wait=0 _pid
	# procd_kill removes the named instance and disables its respawn; signaling
	# the child through ubus would let procd recreate it behind our back.
	procd_kill clashoo singbox >/dev/null 2>&1 || true
	_pid="$(_singbox_instance_pid)"
	[ -n "$_pid" ] || return 0
	while [ "$_wait" -lt 3 ]; do
		_pid="$(_singbox_instance_pid)"
		[ -z "$_pid" ] && return 0
		_singbox_running || return 0
		sleep 1
		_wait=$((_wait + 1))
	done
}

_log_runtime_error() {
	local message="$1"
	echo "$message" > "$REAL_LOG"
	echo "  $(date "+%Y-%m-%d %H:%M:%S") - $message" >> /usr/share/clashoo/clashoo.txt
}

# Run a command with a self-watchdog timeout. busybox has no `timeout` binary,
# so poll the backgrounded pid and kill it on overrun. Returns the command's
# exit status, or 124 when the watchdog fired (timed out).
_run_with_timeout() {
	local _to="$1"; shift
	local _pid _i=0
	"$@" &
	_pid=$!
	while [ "$_i" -lt "$_to" ]; do
		kill -0 "$_pid" 2>/dev/null || { wait "$_pid"; return $?; }
		sleep 1
		_i=$((_i + 1))
	done
	kill -TERM "$_pid" 2>/dev/null
	sleep 1
	kill -KILL "$_pid" 2>/dev/null
	wait "$_pid" 2>/dev/null
	return 124
}

# Pull a concise failure reason out of the core log so users see the real error
# (e.g. "can't download MMDB") instead of only "preflight failed".
_preflight_reason() {
	local line
	line="$(grep -E 'level=(error|fatal)' "$CORE_LOG_PATH" 2>/dev/null | tail -1)"
	[ -n "$line" ] || line="$(tail -1 "$CORE_LOG_PATH" 2>/dev/null)"
	case "$line" in
		*'msg="'*) line="$(printf '%s' "$line" | sed -n 's/.*msg="\([^"]*\)".*/\1/p')" ;;
	esac
	printf '%s' "$line"
}

log_msg() {
	local en_msg="$1" zh_msg="$2"
	local out_msg now
	now="$(date "+%Y-%m-%d %H:%M:%S")"
	if [ "${lang}" = "en" ]; then
		out_msg="$en_msg"
	else
		out_msg="$zh_msg"
	fi
	echo "$out_msg" > "$REAL_LOG"
	[ -n "$out_msg" ] && echo "  ${now} - ${out_msg}" >> /usr/share/clashoo/clashoo.txt
}

runtime_state_reset() {
	rm -f "$RUNTIME_STATE_FILE" >/dev/null 2>&1 || true
}

runtime_state_set() {
	local key="$1" value="${2:-}" safe_value
	[ -n "$key" ] || return 0
	mkdir -p "$(dirname "$RUNTIME_STATE_FILE")" >/dev/null 2>&1
	touch "$RUNTIME_STATE_FILE"
	value="$(printf '%s' "$value" | tr '\r\n' '  ')"
	safe_value="$(printf '%s' "$value" | sed 's/[\/&|\\]/\\&/g')"
	if grep -q "^${key}=" "$RUNTIME_STATE_FILE" 2>/dev/null; then
		sed -i "s|^${key}=.*|${key}=${safe_value}|g" "$RUNTIME_STATE_FILE"
	else
		echo "${key}=${value}" >> "$RUNTIME_STATE_FILE"
	fi
}

runtime_has_tun_device() {
	ip tuntap add mode tun name cotuntest >/dev/null 2>&1 || return 1
	ip link del cotuntest >/dev/null 2>&1 || true
	return 0
}

resolve_effective_tp_modes() {
	local configured_tcp configured_udp
	configured_tcp="$(uci -q get clashoo.config.tcp_mode 2>/dev/null)"
	configured_udp="$(uci -q get clashoo.config.udp_mode 2>/dev/null)"
	[ -z "$configured_tcp" ] && configured_tcp="redirect"
	[ -z "$configured_udp" ] && configured_udp="$configured_tcp"

	EFFECTIVE_TCP_MODE="$configured_tcp"
	EFFECTIVE_UDP_MODE="$configured_udp"
	RUNTIME_DEGRADED="0"
	RUNTIME_DEGRADE_REASON=""
	HAS_TUN_DEVICE="1"

	if ! runtime_has_tun_device; then
		HAS_TUN_DEVICE="0"
		if [ "$EFFECTIVE_TCP_MODE" = "tun" ]; then
			EFFECTIVE_TCP_MODE="redirect"
			RUNTIME_DEGRADED="1"
		fi
		if [ "$EFFECTIVE_UDP_MODE" = "tun" ]; then
			EFFECTIVE_UDP_MODE="tproxy"
			RUNTIME_DEGRADED="1"
		fi
	fi

	[ "$RUNTIME_DEGRADED" = "1" ] && RUNTIME_DEGRADE_REASON="tun_unavailable_auto_fallback"
}

runtime_state_record_preflight() {
	local configured_tcp configured_udp core_family enhanced_mode
	core_family="$1"
	configured_tcp="$(uci -q get clashoo.config.tcp_mode 2>/dev/null)"
	configured_udp="$(uci -q get clashoo.config.udp_mode 2>/dev/null)"
	enhanced_mode="$(uci -q get clashoo.config.enhanced_mode 2>/dev/null)"
	[ -z "$configured_tcp" ] && configured_tcp="redirect"
	[ -z "$configured_udp" ] && configured_udp="$configured_tcp"
	[ -z "$enhanced_mode" ] && enhanced_mode="fake-ip"

	runtime_state_set "configured_core" "$core_family"
	runtime_state_set "configured_tcp_mode" "$configured_tcp"
	runtime_state_set "configured_udp_mode" "$configured_udp"
	runtime_state_set "effective_tcp_mode" "$EFFECTIVE_TCP_MODE"
	runtime_state_set "effective_udp_mode" "$EFFECTIVE_UDP_MODE"
	runtime_state_set "has_tun_device" "$HAS_TUN_DEVICE"
	runtime_state_set "degraded" "$RUNTIME_DEGRADED"
	runtime_state_set "degrade_reason" "$RUNTIME_DEGRADE_REASON"
	runtime_state_set "enhanced_mode" "$enhanced_mode"
	runtime_state_set "health_status" "pending"
	runtime_state_set "health_detail" "init"
	runtime_state_set "updated_at" "$(date +%s)"
}

runtime_state_record_health() {
	local status detail
	status="$1"
	detail="${2:-}"
	[ -z "$status" ] && status="unknown"
	runtime_state_set "health_status" "$status"
	runtime_state_set "health_detail" "$detail"
	runtime_state_set "updated_at" "$(date +%s)"
}

runtime_state_accepts_health_update() {
	_selected_core_running || return 1
	case "$(grep '^health_detail=' "$RUNTIME_STATE_FILE" 2>/dev/null | cut -d= -f2-)" in
		service_stopped|service_disabled|boot_disabled)
			return 1
			;;
	esac
	return 0
}

curl_has_final_http_response() {
	awk '
		/^HTTP\// && $0 !~ /Connection established/ { ok=1 }
		END { exit ok ? 0 : 1 }
	'
}

runtime_run_health_check() {
	local dns_state explicit_state transparent_state detail require_fakeip mixed_port _try
	local dns_out dns_addr _url

	runtime_state_accepts_health_update || return 1

	dns_state="na"
	explicit_state="na"
	transparent_state="na"
	require_fakeip="0"
	[ "$(uci -q get clashoo.config.enable_dns 2>/dev/null)" != "0" ] && [ "$(uci -q get clashoo.config.enhanced_mode 2>/dev/null)" = "fake-ip" ] && require_fakeip="1"

	if command_exists nslookup; then
		dns_state="fail"
		for _try in 1 2 3; do
			if command_exists timeout; then
				dns_out="$(timeout 3 nslookup www.google.com 127.0.0.1 2>/dev/null)"
			else
				dns_out="$(nslookup www.google.com 127.0.0.1 2>/dev/null)"
			fi
			dns_addr="$(echo "$dns_out" | awk '/^Address: /{print $2} /^Address [0-9]+: /{print $3}' | grep -E '^[0-9a-fA-F:.]+$' | tail -n1)"
			if [ -n "$dns_addr" ]; then
				if echo "$dns_addr" | grep -Eq '^198\.18\.|^fc00:'; then
					dns_state="ok"
				else
					dns_state="compat"
				fi
				break
			fi
			sleep 1
		done
	fi

	mixed_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)"
	[ -z "$mixed_port" ] && mixed_port="7890"
	if command_exists curl; then
			explicit_state="fail"
			for _url in https://www.google.com/generate_204 https://www.gstatic.com/generate_204; do
				for _try in 1 2; do
					if curl -4 --proxy "http://127.0.0.1:${mixed_port}" -I --max-time 6 -sS "$_url" 2>/dev/null | curl_has_final_http_response; then
						explicit_state="ok"
						break 2
					fi
					sleep 1
			done
		done
			transparent_state="fail"
			for _url in https://www.google.com https://www.gstatic.com/generate_204; do
				for _try in 1 2; do
					if curl -4 -I --max-time 6 -sS "$_url" 2>/dev/null | curl_has_final_http_response; then
						transparent_state="ok"
						break 2
					fi
					sleep 1
			done
		done
	fi

	detail="dns:${dns_state};explicit:${explicit_state};transparent:${transparent_state}"
	# Pass on explicit (local mixed-port) reachability alone. The transparent
	# probe tests the router's OWN output-redirect path, which is unreliable on
	# some stacks (PPPoE/GL.iNet, issue #25) yet does not reflect LAN-client
	# proxying — keep it informational in `detail`, not a pass gate.
	if [ "$explicit_state" = "ok" ]; then
		if [ "$require_fakeip" = "0" ] || [ "$dns_state" = "ok" ] || [ "$dns_state" = "compat" ] || [ "$dns_state" = "na" ]; then
			runtime_state_accepts_health_update || return 1
			runtime_state_record_health "pass" "$detail"
			return 0
		fi
	fi

	runtime_state_accepts_health_update || return 1
	runtime_state_record_health "fail" "$detail"
	echo "  $(date "+%Y-%m-%d %H:%M:%S") - runtime health check failed: ${detail}" >> /usr/share/clashoo/clashoo.txt
	return 1
}

# check now; on fail detach a bounded re-check so cold-start false-fails self-correct
runtime_run_health_check_settle() {
	runtime_run_health_check && return 0
	local lock="/tmp/clashoo_health_recheck.lock"
	( set -C; echo "$$" >"$lock" ) 2>/dev/null || return 1
	(
		exec 1000>&- 2>/dev/null
		trap 'rm -f "$lock"' EXIT
		_i=0
		while [ "$_i" -lt 8 ]; do
			sleep 15
			_selected_core_running || break
			runtime_run_health_check && break
			_i=$((_i + 1))
		done
	) </dev/null >/dev/null 2>&1 &
	return 1
}

_require_runtime_network_stack() {
	local missing=""

	for _cmd in fw4 nft ip; do
		command_exists "$_cmd" || missing="$missing $_cmd"
	done

	if [ -n "$missing" ]; then
		_log_runtime_error "Missing required fw4 runtime tools:$missing"
		return 1
	fi

	return 0
}

_service_enabled() {
	local svc="$1"
	[ -x "/etc/init.d/${svc}" ] || return 1
	"/etc/init.d/${svc}" enabled >/dev/null 2>&1
}

# auto-detect core binary by priority: mihomo > clash-meta > built-in clash
_detect_core() {
	for _bin in $(_core_candidates); do
		[ -x "$_bin" ] && echo "$_bin" && return
	done
	echo "/etc/clashoo/clash"
}
CLASH="$(_detect_selected_core)"
CLASH_CONFIG="/etc/clashoo"
CRON_FILE="/etc/crontabs/root"
CONFIG_YAML="/etc/clashoo/config.yaml"

CUSLIST="/tmp/dnsmasq.d/custom_list.conf"
CUSLITT="/tmp/dnsmasq.clashoo"
CUSLISTV="/var/dnsmasq.d/custom_list.conf" 
CUSLITTV="/var/dnsmasq.clashoo" 
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
RUNTIME_STATE_FILE="/tmp/clashoo/runtime_state"
DNS_STATE_DIR="/tmp/clashoo"
DNS_STATE_SERVER_FILE="${DNS_STATE_DIR}/dnsmasq_server.before"
DNS_STATE_NORESOLV_FILE="${DNS_STATE_DIR}/dnsmasq_noresolv.before"
DNS_STATE_CACHE_FILE="${DNS_STATE_DIR}/dnsmasq_cachesize.before"
DNS_STATE_RESOLV_FILE="${DNS_STATE_DIR}/dnsmasq_resolvfile.before"

cleanup_legacy_startup_backups() {
#===========================================================================================================================
	# Historical backup artifacts can clutter LuCI "System -> Startup".
	# Keep runtime clean by removing known obsolete backup filenames.
	# The old `*bak*` glob also wiped other packages' backups (e.g. wireless.bak),
	# so scope it to Clashoo's own script names (legacy `clash`, current `clashoo`).
	# `._*` is macOS AppleDouble metadata, never a valid init script, so it stays.
	rm -f /etc/init.d/._clash \
	      /etc/init.d/._* \
	      /etc/init.d/clash*bak* >/dev/null 2>&1 || true
#===========================================================================================================================
}

migrate_legacy_clash_dir() {
	local legacy_dir target_dir legacy_path target_path legacy_name
	legacy_dir="/etc/clash"
	target_dir="/etc/clashoo"

	[ -d "$legacy_dir" ] || return 0
	mkdir -p "$target_dir" >/dev/null 2>&1

	for legacy_name in config.yaml Country.mmdb geoip.metadb geosite.dat geoip.dat GeoLite2-ASN.mmdb; do
		legacy_path="${legacy_dir}/${legacy_name}"
		target_path="${target_dir}/${legacy_name}"
		[ -e "$legacy_path" ] || continue
		[ -e "$target_path" ] || mv -f "$legacy_path" "$target_path" >/dev/null 2>&1
	done

	for legacy_name in rules ruleset rule-set profiles; do
		legacy_path="${legacy_dir}/${legacy_name}"
		target_path="${target_dir}/${legacy_name}"
		[ -e "$legacy_path" ] || continue
		[ -e "$target_path" ] || mv -f "$legacy_path" "$target_path" >/dev/null 2>&1
	done

	rmdir "$legacy_dir" >/dev/null 2>&1 || true
}

backup_dnsmasq_state() {
#===========================================================================================================================
	[ -f "$DNS_STATE_SERVER_FILE" ] && return 0
	mkdir -p "$DNS_STATE_DIR" >/dev/null 2>&1
	uci -q get dhcp.@dnsmasq[0].server >"$DNS_STATE_SERVER_FILE" 2>/dev/null || : >"$DNS_STATE_SERVER_FILE"
	uci -q get dhcp.@dnsmasq[0].noresolv >"$DNS_STATE_NORESOLV_FILE" 2>/dev/null || : >"$DNS_STATE_NORESOLV_FILE"
	uci -q get dhcp.@dnsmasq[0].cachesize >"$DNS_STATE_CACHE_FILE" 2>/dev/null || : >"$DNS_STATE_CACHE_FILE"
	uci -q get dhcp.@dnsmasq[0].resolvfile >"$DNS_STATE_RESOLV_FILE" 2>/dev/null || : >"$DNS_STATE_RESOLV_FILE"
#===========================================================================================================================
}

add_clash_dns_server() {
#===========================================================================================================================
	local dns_port="$1"
	printf '%s' "$dns_port" | grep -Eq '^[0-9]+$' || return 1
	uci del_list dhcp.@dnsmasq[0].server="127.0.0.1#${dns_port}" >/dev/null 2>&1 || true
	uci add_list dhcp.@dnsmasq[0].server="127.0.0.1#${dns_port}" >/dev/null 2>&1
#===========================================================================================================================
}

restore_dnsmasq_state() {
#===========================================================================================================================
	local token saved
	[ -d "$DNS_STATE_DIR" ] || return 0

	if [ -f "$DNS_STATE_SERVER_FILE" ]; then
		saved="$(cat "$DNS_STATE_SERVER_FILE" 2>/dev/null)"
		uci -q delete dhcp.@dnsmasq[0].server >/dev/null 2>&1
		for token in $saved; do
			uci add_list dhcp.@dnsmasq[0].server="$token" >/dev/null 2>&1
		done
	fi

	if [ -s "$DNS_STATE_NORESOLV_FILE" ]; then
		uci set dhcp.@dnsmasq[0].noresolv="$(cat "$DNS_STATE_NORESOLV_FILE" 2>/dev/null)" >/dev/null 2>&1
	else
		uci -q delete dhcp.@dnsmasq[0].noresolv >/dev/null 2>&1
	fi

	if [ -s "$DNS_STATE_CACHE_FILE" ]; then
		uci set dhcp.@dnsmasq[0].cachesize="$(cat "$DNS_STATE_CACHE_FILE" 2>/dev/null)" >/dev/null 2>&1
	else
		uci -q delete dhcp.@dnsmasq[0].cachesize >/dev/null 2>&1
	fi

	if [ -s "$DNS_STATE_RESOLV_FILE" ]; then
		uci set dhcp.@dnsmasq[0].resolvfile="$(cat "$DNS_STATE_RESOLV_FILE" 2>/dev/null)" >/dev/null 2>&1
	else
		uci -q delete dhcp.@dnsmasq[0].resolvfile >/dev/null 2>&1
	fi

	rm -f "$DNS_STATE_SERVER_FILE" "$DNS_STATE_NORESOLV_FILE" "$DNS_STATE_CACHE_FILE" "$DNS_STATE_RESOLV_FILE" >/dev/null 2>&1
	rmdir "$DNS_STATE_DIR" >/dev/null 2>&1 || true
#===========================================================================================================================
}

ensure_dns_when_core_stopped() {
#===========================================================================================================================
	local force="${1:-0}" servers token remote_servers
	[ "$force" = "1" ] || ! _selected_core_running || return 0
	servers="$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null)"
	remote_servers=""
	for token in $servers; do
		case "$token" in
			127.0.0.1#1053|127.0.0.1#5300|127.0.0.1#7874) ;;
			*) remote_servers="${remote_servers} ${token}" ;;
		esac
	done

	if [ "$force" = "1" ] || [ -z "$remote_servers" ]; then
		uci -q delete dhcp.@dnsmasq[0].server >/dev/null 2>&1
		for token in $remote_servers; do
			uci add_list dhcp.@dnsmasq[0].server="$token" >/dev/null 2>&1
		done
	fi
	if [ -z "$remote_servers" ]; then
		uci add_list dhcp.@dnsmasq[0].server='119.29.29.29' >/dev/null 2>&1
		uci add_list dhcp.@dnsmasq[0].server='223.5.5.5' >/dev/null 2>&1
	fi
	if [ "$force" = "1" ] || [ -z "$remote_servers" ]; then
		uci set dhcp.@dnsmasq[0].noresolv='0' >/dev/null 2>&1
	fi
#===========================================================================================================================
}

_dnsmasq_state_digest() {
	local _nr _cs
	_nr="$(uci -q get dhcp.@dnsmasq[0].noresolv 2>/dev/null)"
	_cs="$(uci -q get dhcp.@dnsmasq[0].cachesize 2>/dev/null)"
	case "$_nr" in ''|0|false) _nr=0 ;; *) _nr=1 ;; esac
	[ -n "$_cs" ] || _cs=default
	printf '%s|%s|%s|%s' \
		"$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null)" \
		"$_nr" "$_cs" \
		"$(uci -q get dhcp.@dnsmasq[0].resolvfile 2>/dev/null)"
}

_reload_dnsmasq() {
	/etc/init.d/dnsmasq reload >/dev/null 2>&1 && return 0
	/etc/init.d/dnsmasq restart >/dev/null 2>&1
}

revert_dns() {
#===========================================================================================================================  
	local _before _after
	_before="$(_dnsmasq_state_digest)"
	restore_dnsmasq_state
	ensure_dns_when_core_stopped "${1:-0}"
	rm -rf "$CUSLIST" "$CUSLITT" "$CUSLISTV" "$CUSLITTV" 2>/dev/null
	_after="$(_dnsmasq_state_digest)"
	if [ "$_before" = "$_after" ]; then
		uci -q revert dhcp
		return 0
	fi
	uci commit dhcp
	_reload_dnsmasq >/dev/null 2>&1
#=========================================================================================================================== 	 
}

add_cron(){
#===========================================================================================================================
	# core.log 落在 tmpfs 上，按分钟卡一次大小；内核对日志没有上限，
	# 一旦刷屏写满 /tmp，dnsmasq 与配置生成都会跟着挂。
	[ -z "$(grep -w "lib/core_log_guard.sh" "$CRON_FILE")" ] && echo "* * * * * sh /usr/share/clashoo/lib/core_log_guard.sh >/dev/null 2>&1" >> "$CRON_FILE"

	auto=$(uci get clashoo.config.auto_update 2>/dev/null)
	clear=$(uci get clashoo.config.auto_clear_log 2>/dev/null)
	if [ "${auto:-0}" -eq 1 ] || [ "${clear:-0}" -eq 1 ]; then
		[ -z "$(grep -w "/usr/share/clashoo/update/maintenance_cron.sh" "$CRON_FILE")" ] && echo "23 * * * * sh /usr/share/clashoo/update/maintenance_cron.sh >/dev/null 2>&1" >> "$CRON_FILE"
	fi

	sub_auto=$(uci get clashoo.config.auto_subscription_update 2>/dev/null)
	if [ "${sub_auto:-0}" -eq 1 ]; then
		[ -z "$(grep -w "/usr/share/clashoo/update/subscription_update_cron.sh" "$CRON_FILE")" ] && echo "7 * * * * sh /usr/share/clashoo/update/subscription_update_cron.sh >/dev/null 2>&1" >> "$CRON_FILE"
	fi
	
	lgbm_auto=$(uci get clashoo.config.smart_lgbm_auto_update 2>/dev/null)
	if [ "${lgbm_auto:-0}" -eq 1 ]; then
		lgbm_interval=$(uci get clashoo.config.smart_lgbm_update_interval 2>/dev/null)
		echo "$lgbm_interval" | grep -Eq '^[0-9]+$' || lgbm_interval=72
		[ "$lgbm_interval" -lt 1 ] && lgbm_interval=72
		lgbm_days=$(( lgbm_interval / 24 ))
		[ "$lgbm_days" -lt 1 ] && lgbm_days=1
		[ -z "$(grep -w "update/lgbm_update.sh" "$CRON_FILE")" ] && echo "0 3 */$lgbm_days * * sh /usr/share/clashoo/update/lgbm_update.sh >/dev/null 2>&1" >> "$CRON_FILE"
	fi
	crontab "$CRON_FILE"
#=========================================================================================================================== 	
}

del_cron(){
#=========================================================================================================================== 
	sed -i '/update\/geoip.sh/d' "$CRON_FILE"
	sed -i '/rpc_async.sh update_geoip/d' "$CRON_FILE"
	sed -i '/\/usr\/share\/clash\/geoip.sh/d' "$CRON_FILE"
	sed -i '/update\/update_all.sh/d' "$CRON_FILE"
	sed -i '/\/usr\/share\/clash\/update_all.sh/d' "$CRON_FILE"
	sed -i '/update\/maintenance_cron.sh/d' "$CRON_FILE"
	sed -i '/lib\/core_log_guard.sh/d' "$CRON_FILE"
	sed -i '/clashoo.txt/d' "$CRON_FILE"
	sed -i '/update\/lgbm_update.sh/d' "$CRON_FILE"
	sed -i '/\/usr\/share\/clash\/lgbm_update.sh/d' "$CRON_FILE"
	sed -i '/update\/subscription_update_cron.sh/d' "$CRON_FILE"
	/etc/init.d/cron restart
#=========================================================================================================================== 	
}

refresh_cron() {
	del_cron >/dev/null 2>&1
	[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ] && _selected_core_running && add_cron >/dev/null 2>&1
	return 0
}


select_config(){
#=========================================================================================================================== 
CONFIG_YAML_PATH=$(uci get clashoo.config.use_config 2>/dev/null)
log_msg "Checking Config file" "正在检查配置文件"

# Runtime dir may be missing (e.g. after a broken uninstall left the package
# installed but its /etc/clashoo dir removed). Recreate it so the cp below
# can't silently fail and leave the core without a config.
mkdir -p "$(dirname "$CONFIG_YAML")" 2>/dev/null

if [ -f "$CONFIG_YAML_PATH" ] && [ -s "$CONFIG_YAML_PATH" ];then

	if ! cp "$CONFIG_YAML_PATH" "$CONFIG_YAML" 2>/dev/null || [ ! -s "$CONFIG_YAML" ];then
		log_msg "Failed to stage config file" "配置写入失败：运行目录缺失或不可写"
		sleep 5
		echo "Clashoo" >$REAL_LOG
		return 1
	fi

elif [ ! -f "$CONFIG_YAML_PATH" ] && [ ! -f "$CONFIG_YAML" ];then

		log_msg "No config found" "找不到配置文章"
		sleep 5
		echo "Clashoo" >$REAL_LOG
		return 1	
	
elif [ ! -s "$CONFIG_YAML_PATH" ] && [ ! -s "$CONFIG_YAML" ];then

		log_msg "Your Config File is Empty" "你的config.yaml有问题还是为了空"
		sleep 5
		echo "Clashoo" >$REAL_LOG
		return 1	
		
fi

#=========================================================================================================================== 
}



_dns_roles_look_broken() {
	local i=0 t a en broken=1 fb_total=0 fb_domestic=0
	while :; do
		t="$(uci -q get clashoo.@dnsservers[$i].ser_type 2>/dev/null)"
		[ -n "$t" ] || break
		en="$(uci -q get clashoo.@dnsservers[$i].enabled 2>/dev/null)"
		a="$(uci -q get clashoo.@dnsservers[$i].ser_address 2>/dev/null)"
		i=$((i + 1))
		[ "$en" = "0" ] && continue
		case "$t" in
			nameserver)
				printf '%s' "$a" | grep -qE '1\.1\.1\.1|1\.0\.0\.1|one\.one\.one\.one|cloudflare|8\.8\.8\.8|8\.8\.4\.4|dns\.google|9\.9\.9\.9' && broken=0
				;;
			fallback)
				fb_total=$((fb_total + 1))
				printf '%s' "$a" | grep -qE '223\.5\.5\.5|223\.6\.6\.6|119\.29\.29\.29|119\.28\.28\.28|114\.114|180\.184|alidns|doh\.pub|dnspod' && fb_domestic=$((fb_domestic + 1))
				;;
		esac
	done
	[ "$fb_total" -gt 0 ] && [ "$fb_total" = "$fb_domestic" ] && broken=0
	return $broken
}

_dns_drop_legacy_geolocation_policy() {
	local i=0 matcher nameserver
	while uci -q get "clashoo.@dns_policy[$i]" >/dev/null 2>&1; do
		matcher="$(uci -q get "clashoo.@dns_policy[$i].matcher" 2>/dev/null)"
		nameserver="$(uci -q get "clashoo.@dns_policy[$i].nameserver" 2>/dev/null)"
		if [ "$matcher" = "geosite:geolocation-!cn" ] && [ "$nameserver" = "https://cloudflare-dns.com/dns-query" ]; then
			uci -q delete "clashoo.@dns_policy[$i]"
			continue
		fi
		i=$((i + 1))
	done
}

_migrate_dns_roles() {
	local ver
	ver="$(uci -q get clashoo.config.dns_migrated 2>/dev/null)"
	[ "$ver" = "3" ] && return 0
	if [ "$ver" = "2" ]; then
		_dns_drop_legacy_geolocation_policy
		uci -q set clashoo.config.dns_migrated='3'
		uci -q commit clashoo
		return 0
	fi
	if [ "$ver" = "1" ] && ! _dns_roles_look_broken; then
		_dns_drop_legacy_geolocation_policy
		uci -q set clashoo.config.dns_migrated='3'
		uci -q commit clashoo
		return 0
	fi
	while uci -q delete clashoo.@dnsservers[0] >/dev/null 2>&1; do :; done
	while uci -q delete clashoo.@dns_policy[0] >/dev/null 2>&1; do :; done
	_add_dnsserver() {
		local sec
		sec="$(uci -q add clashoo dnsservers)" || return 1
		uci -q set clashoo."$sec".enabled='1'
		uci -q set clashoo."$sec".ser_type="$1"
		uci -q set clashoo."$sec".ser_address="$2"
		uci -q set clashoo."$sec".protocol="${3:-none}"
		[ -n "$4" ] && uci -q set clashoo."$sec".ser_port="$4"
	}
	_add_dnsserver 'nameserver' 'https://dns.alidns.com/dns-query'
	_add_dnsserver 'nameserver' 'https://doh.pub/dns-query'
	_add_dnsserver 'proxy-server-nameserver' '1.1.1.1' 'tls://' '853'
	_add_dnsserver 'fallback' 'https://cloudflare-dns.com/dns-query'
	_add_dnsserver 'fallback' 'https://dns.google/dns-query'
	_add_dnsserver 'direct-nameserver' '223.5.5.5' 'udp://'
	local sec
	sec="$(uci -q add clashoo dns_policy)"
	uci -q set clashoo."$sec".enabled='1'
	uci -q set clashoo."$sec".policy_type='nameserver-policy'
	uci -q set clashoo."$sec".matcher='geosite:cn'
	uci -q add_list clashoo."$sec".nameserver='https://dns.alidns.com/dns-query'
	uci -q add_list clashoo."$sec".nameserver='https://doh.pub/dns-query'
	uci -q set clashoo.config.dns_migrated='3'
	uci -q commit clashoo
	log_msg "Upstream DNS roles migrated" "上游 DNS 角色已恢复为经典防污染模型（国内主解析 + 境外 fallback）"
}

_user_dns_fields() {
	local field out="" force="${1:-}"
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0
	for field in nameserver proxy-server-nameserver fallback direct-nameserver \
		default-nameserver nameserver-policy proxy-server-nameserver-policy respect-rules; do
		case ",$force," in *,"$field",*) continue ;; esac
		[ "$(yq -M e "(.dns // {}) | has(\"${field}\")" "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
			out="${out}${out:+,}${field}"
	done
	[ "$(yq -M e '.dns."prefer-h3" // false' "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
		out="${out}${out:+,}prefer-h3"
	printf '%s' "$out"
}

_dns_list_values() {
	local field="$1"
	command -v yq >/dev/null 2>&1 || return 0
	yq -M e -r "(.dns.\"${field}\" // [])[]" "$CONFIG_YAML" 2>/dev/null | sed '/^null$/d'
}

_dns_value_is_domestic() {
	case "$1" in
		*223.5.5.5*|*119.29.29.29*|*114.114.114.114*|*180.184.*|*1.2.4.8*|*doh.pub*|*alidns*|*dns.pub*|*dnspod*)
			return 0 ;;
	esac
	return 1
}

_dns_list_all_domestic() {
	local field="$1" value seen=0
	for value in $(_dns_list_values "$field"); do
		seen=1
		_dns_value_is_domestic "$value" || return 1
	done
	[ "$seen" = "1" ]
}

_dns_has_cn_rule_set() {
	_dns_list_values "fake-ip-filter" | grep -qx 'rule-set:cn_domain' || return 1
	yq -M e -r '(.dns."nameserver-policy"."rule-set:cn_domain" // [])[]' "$CONFIG_YAML" 2>/dev/null | grep -q .
}

_clashoo_dns_force_fields() {
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0
	local managed legacy fields
	fields="nameserver,proxy-server-nameserver,direct-nameserver,default-nameserver,nameserver-policy,proxy-server-nameserver-policy,respect-rules"
	managed="$(yq -M e '(.dns."x-clashoo-managed-dns" // false) == true' "$CONFIG_YAML" 2>/dev/null)"
	if [ "$managed" = "true" ]; then
		printf '%s' "$fields"
		return 0
	fi
	legacy=0
	if _dns_has_cn_rule_set &&
		_dns_list_all_domestic "nameserver" &&
		_dns_list_all_domestic "proxy-server-nameserver"; then
		legacy=1
	fi
	[ "$legacy" = "1" ] && printf '%s' "$fields"
	return 0
}

_dns_nameserver_inherit_mode() {
	local has_nameserver marker prefer_h3 respect_false has_proxy force_respect
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0

	has_nameserver="$(yq -M e '(.dns.nameserver // null) | (tag != "!!null" and length > 0)' "$CONFIG_YAML" 2>/dev/null)"
	[ "$has_nameserver" = "true" ] || return 0

	marker=0
	if yq -M e -r '(.dns.nameserver // null) | .. | select(tag == "!!str")' "$CONFIG_YAML" 2>/dev/null |
		awk '
		{
			hash = index($0, "#")
			if (!hash) next
			fragment = substr($0, hash + 1)
			split(fragment, parts, "&")
			if (parts[1] !~ /=/) { found = 1; exit }
		}
		END { exit(found ? 0 : 1) }'; then
		marker=1
	fi
	prefer_h3="$(yq -M e '(.dns."prefer-h3" // false) == true' "$CONFIG_YAML" 2>/dev/null)"
	respect_false="$(yq -M e '((.dns // {}) | has("respect-rules")) and ((.dns."respect-rules") == false or (.dns."respect-rules") == "false")' "$CONFIG_YAML" 2>/dev/null)"
	has_proxy="$(yq -M e '(.dns // {}) | has("proxy-server-nameserver")' "$CONFIG_YAML" 2>/dev/null)"
	force_respect=0
	case ",${CLASHOO_DNS_FORCE:-}," in
		*,respect-rules,*) force_respect=1 ;;
	esac

	# skip suppresses both the generated proxy role and automatic respect-rules.
	if [ "$marker" = "1" ] ||
		{ [ "$prefer_h3" = "true" ] && [ "$force_respect" != "1" ]; } ||
		{ [ "$respect_false" = "true" ] && [ "$force_respect" != "1" ]; } ||
		[ "$(uci -q get clashoo.config.dns_respect_rules 2>/dev/null)" = "0" ]; then
		printf '%s' skip
		return 0
	fi
	case ",${CLASHOO_DNS_FORCE:-}," in
		*,proxy-server-nameserver,*) return 0 ;;
	esac
	[ "$has_proxy" = "true" ] && return 0
	printf '%s' inherit
}

# issue #73: mihomo resolves proxy server domains through
# proxy-server-nameserver(-policy) and ignores nameserver-policy there. Airports
# ship their own resolver for node domains (e.g. "+.chylink.xyz" -> DoT) which
# answers with reachable relay IPs, so mirror the entries that match a node
# server domain before mixin drops them.
#
# Everything travels through a private 0700 work dir: large airports can produce
# hundreds of KB of policy entries, which neither argv nor env vars can carry
# (E2BIG), and predictable /tmp names are symlink bait.
_dns_node_policy_workdir=""

_dns_node_policy_cleanup() {
	[ -n "${_dns_node_policy_workdir:-}" ] || return 0
	rm -rf "$_dns_node_policy_workdir" 2>/dev/null
	_dns_node_policy_workdir=""
	return 0
}

# Same inputs yml_change feeds mixin: settle FORCE/PRESENT here so the skip
# decision cannot disagree with the config that mixin actually generates.
_dns_node_policy_mode() {
	CLASHOO_DNS_FORCE="$(_clashoo_dns_force_fields)"
	CLASHOO_DNS_PRESENT="$(_user_dns_fields "$CLASHOO_DNS_FORCE")"
	export CLASHOO_DNS_FORCE CLASHOO_DNS_PRESENT
	_dns_nameserver_inherit_mode
}

_dns_node_policy_capture() {
	_dns_node_policy_cleanup
	command -v yq >/dev/null 2>&1 || return 0
	command -v ucode >/dev/null 2>&1 || return 0

	local helper workdir
	helper="${CLASHOO_NODE_DNS_UC:-/usr/share/clashoo/runtime/dns_node_policy.uc}"
	[ -r "$helper" ] || return 0
	[ -s "$CONFIG_YAML" ] || return 0

	# skip mode keeps the source DNS verbatim: mihomo already falls back to the
	# normal DNS policy for node domains when no dedicated resolver is generated.
	[ "$(_dns_node_policy_mode)" = "skip" ] && return 0

	# sweep leftovers from an interrupted start; they are private to root anyway
	find "${TMPDIR:-/tmp}" -maxdepth 1 -name 'clashoo-node-dns.*' -mmin +60 \
		-exec rm -rf {} + >/dev/null 2>&1

	workdir="$(mktemp -d "${TMPDIR:-/tmp}/clashoo-node-dns.XXXXXX" 2>/dev/null)" || return 0
	chmod 700 "$workdir" 2>/dev/null

	if ! yq -M e -o=json -I0 '({"policies": (.dns."nameserver-policy" // {}), "servers": [(.proxies // [])[] | select(type == "!!map") | .server // ""]})' "$CONFIG_YAML" > "$workdir/source.json" 2>/dev/null ||
		[ ! -s "$workdir/source.json" ]; then
		rm -rf "$workdir"
		return 0
	fi
	chmod 600 "$workdir/source.json" 2>/dev/null

	if ! CLASHOO_NODE_DNS_SOURCE_FILE="$workdir/source.json" ucode "$helper" > "$workdir/policy.json" 2>/dev/null ||
		[ ! -s "$workdir/policy.json" ]; then
		rm -rf "$workdir"
		return 0
	fi
	chmod 600 "$workdir/policy.json" 2>/dev/null
	rm -f "$workdir/source.json"

	if [ "$(yq -M e -r 'length' "$workdir/policy.json" 2>/dev/null)" = "0" ]; then
		rm -rf "$workdir"
		return 0
	fi

	_dns_node_policy_workdir="$workdir"
	return 0
}

_dns_node_policy_apply() {
	local policy_file rc
	policy_file="${_dns_node_policy_workdir:+"$_dns_node_policy_workdir/policy.json"}"
	if [ -z "$policy_file" ] || [ ! -s "$policy_file" ] || ! command -v yq >/dev/null 2>&1; then
		_dns_node_policy_cleanup
		return 0
	fi

	# existing keys keep their value; overlapping patterns are left to mihomo
	CLASHOO_NODE_DNS_POLICY_FILE="$policy_file" yq -M e -i '(.dns // {}) as $dns |
		(load(strenv(CLASHOO_NODE_DNS_POLICY_FILE)) // {}) as $derived |
		.dns."proxy-server-nameserver-policy" = ($derived * ($dns."proxy-server-nameserver-policy" // {}))
	' "$CONFIG_YAML" 2>/dev/null
	rc=$?

	# mihomo rejects a node policy without a proxy node resolver
	if [ "$(yq -M e -r '((.dns."proxy-server-nameserver" // []) | length)' "$CONFIG_YAML" 2>/dev/null)" = "0" ]; then
		yq -M e -i '.dns."proxy-server-nameserver" = (.dns."default-nameserver" // ["223.5.5.5"])' "$CONFIG_YAML" 2>/dev/null || true
	fi

	_dns_node_policy_cleanup
	[ "$rc" = "0" ] && log_msg "Inherited node domain DNS policy from subscription" "已从订阅继承节点域名 DNS 策略"
	return 0
}

_dns_proxy_nameservers() {
	local i=0 t en a p port s out="" prefix
	while :; do
		t="$(uci -q get clashoo.@dnsservers[$i].ser_type 2>/dev/null)"
		[ -n "$t" ] || break
		en="$(uci -q get clashoo.@dnsservers[$i].enabled 2>/dev/null)"
		a="$(uci -q get clashoo.@dnsservers[$i].ser_address 2>/dev/null)"
		p="$(uci -q get clashoo.@dnsservers[$i].protocol 2>/dev/null)"
		port="$(uci -q get clashoo.@dnsservers[$i].ser_port 2>/dev/null)"
		i=$((i + 1))
		[ "$t" = "proxy-server-nameserver" ] || continue
		[ "$en" = "0" ] && continue
		if printf '%s' "$a" | grep -Eq '^[A-Za-z][A-Za-z0-9+.-]*://'; then
			s="$a"
		else
			case "$p" in
				''|none) prefix='' ;;
				udp|udp://) prefix='udp://' ;;
				tcp|tcp://) prefix='tcp://' ;;
				dot|tls|tls://) prefix='tls://' ;;
				doh|https|https://) prefix='https://' ;;
				doq|quic|quic://) prefix='quic://' ;;
				*) prefix="$p" ;;
			esac
			s="${prefix}${a}"
			[ -n "$port" ] && s="${s}:${port}"
		fi
		[ -n "$s" ] && out="${out}${out:+ }${s}"
	done
	printf '%s' "$out"
}

_adapt_dns_loopback_resolver() {
	local filter listen host host_pattern old_port servers replacement tmp
	[ "$(uci -q get clashoo.config.dns_loopback_compat 2>/dev/null)" = "1" ] || return 0
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0

	filter="${CLASHOO_DNS_LOOPBACK_FILTER:-/usr/share/clashoo/runtime/dns_loopback_adapter.yq}"
	[ -r "$filter" ] || return 0
	[ "$(yq -M e '(.dns."proxy-server-nameserver" | type)' "$CONFIG_YAML" 2>/dev/null)" = "!!seq" ] || return 0

	listen="$(yq -M e -r '.dns.listen // ""' "$CONFIG_YAML" 2>/dev/null)"
	old_port="${listen##*:}"
	host="${listen%:*}"
	printf '%s' "$old_port" | grep -Eq '^[0-9]+$' || return 0
	host="$(printf '%s' "$host" | tr 'A-Z' 'a-z')"
	case "$host" in
		""|0.0.0.0|'[::]'|::)
			host_pattern='(localhost|127(\.[0-9]{1,3}){3}|\[?::1\]?)' ;;
		localhost)
			host_pattern='localhost' ;;
		127.*)
			printf '%s' "$host" | grep -Eq '^127(\.[0-9]{1,3}){3}$' || return 0
			host_pattern="$(printf '%s' "$host" | sed 's/\./\\./g')" ;;
		'[::1]'|::1)
			host_pattern='\[?::1\]?' ;;
		*)
			return 0 ;;
	esac

	servers="$(yq -M e -r '.dns."proxy-server-nameserver"[]' "$CONFIG_YAML" 2>/dev/null)"
	printf '%s\n' "$servers" | grep -Eiq "^([A-Za-z][A-Za-z0-9+.-]*://)?(${host_pattern}):${old_port}($|[/#?])" || return 0

	replacement="$(_dns_proxy_nameservers)"
	[ -n "$replacement" ] || replacement='tls://1.1.1.1:853'
	tmp="$(mktemp "${CONFIG_YAML}.dns-loopback.XXXXXX" 2>/dev/null)" || return 0
	export CLASHOO_DNS_OLD_PORT="$old_port"
	export CLASHOO_DNS_HOST_PATTERN="$host_pattern"
	export CLASHOO_DNS_REPLACEMENT="$replacement"
	if yq -M e --from-file "$filter" "$CONFIG_YAML" > "$tmp" 2>/dev/null &&
		mv -f "$tmp" "$CONFIG_YAML"; then
		log_msg "Adapted stale loopback proxy DNS" "已适配失效的回环节点 DNS"
	else
		rm -f "$tmp"
		log_msg "Loopback proxy DNS adaptation failed; preserved source settings" "回环节点 DNS 适配失败，已保留原配置"
	fi
	unset CLASHOO_DNS_OLD_PORT CLASHOO_DNS_HOST_PATTERN CLASHOO_DNS_REPLACEMENT
	return 0
}

backfill_custom_sniffer_overrides() {
	[ "$CLASHOO_KEEP_SNIFFER" = "1" ] || return 0
	[ "$(uci -q get clashoo.config.sniffer_streaming 2>/dev/null)" = "1" ] || return 0
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0
	[ -r /usr/share/clashoo/runtime/sniffer_overrides.yq ] || return 0
	yq -M e -i --from-file /usr/share/clashoo/runtime/sniffer_overrides.yq "$CONFIG_YAML" 2>/dev/null || true
}

ensure_quic_fallback_guard() {
	[ "$(uci -q get clashoo.config.sniffer_streaming 2>/dev/null)" = "1" ] || return 0
	[ "${EFFECTIVE_UDP_MODE:-off}" != "off" ] || return 0
	[ "$(uci -q get clashoo.config.block_quic 2>/dev/null)" != "1" ] || return 0
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0
	[ -r /usr/share/clashoo/runtime/quic_fallback_guard.yq ] || return 0
	yq -M e -i --from-file /usr/share/clashoo/runtime/quic_fallback_guard.yq "$CONFIG_YAML" 2>/dev/null || true
}

yml_change() {
	CLASHOO_DNS_FORCE="$(_clashoo_dns_force_fields)"
	CLASHOO_DNS_PRESENT="$(_user_dns_fields "$CLASHOO_DNS_FORCE")"
	CLASHOO_DNS_INHERIT_MODE="$(_dns_nameserver_inherit_mode)"
	CLASHOO_DNS_INHERIT_NAMESERVER=""
	if [ "$CLASHOO_DNS_INHERIT_MODE" = "inherit" ]; then
		CLASHOO_DNS_INHERIT_NAMESERVER="$(yq -M e -o=json '(.dns.nameserver // [])' "$CONFIG_YAML" 2>/dev/null)"
	fi
	export CLASHOO_DNS_FORCE
	export CLASHOO_DNS_PRESENT
	export CLASHOO_DNS_INHERIT_MODE
	export CLASHOO_DNS_INHERIT_NAMESERVER
	CLASHOO_KEEP_FAKEIP_FILTER=""
	CLASHOO_KEEP_SNIFFER=""
	if command -v yq >/dev/null 2>&1 && [ -s "$CONFIG_YAML" ]; then
		[ "$(yq -M e '(.dns // {}) | has("fake-ip-filter")' "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
			CLASHOO_KEEP_FAKEIP_FILTER=1
		[ "$(yq -M e 'has("sniffer")' "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
			CLASHOO_KEEP_SNIFFER=1
	fi
	export CLASHOO_KEEP_FAKEIP_FILTER
	export CLASHOO_KEEP_SNIFFER
	ucode /usr/share/clashoo/runtime/mixin.uc 2>/dev/null | yq -M -p json -o yaml > /tmp/_clashoo_mixin.yaml 2>/dev/null &&
		yq -M eval-all '. as $item ireduce ({}; . * $item)' "$CONFIG_YAML" /tmp/_clashoo_mixin.yaml > "${CONFIG_YAML}.mixin" 2>/dev/null && 
		mv -f "${CONFIG_YAML}.mixin" "$CONFIG_YAML" 2>/dev/null && 
		rm -f /tmp/_clashoo_mixin.yaml 2>/dev/null
	backfill_custom_sniffer_overrides
	ensure_quic_fallback_guard
}

_uci_fake_ip_filter_extras() {
	local item list
	list="$(uci -q get clashoo.config.fake_ip_filter 2>/dev/null)"
	set -f
	for item in $list; do
		case "$item" in
			'*.lan'|'localhost.ptlogin2.qq.com')
				continue ;;
			geosite:cn)
				printf '%s\n' 'rule-set:cn_domain' ;;
			*)
				printf '%s\n' "$item" ;;
		esac
	done
	set +f
}

drop_legacy_cn_ip_injection() {
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0

	if ! yq -M e -i '
		. as $doc |
		(select(
			(.["rule-providers"].clashoo_cn_ip | length) == 4 and
			.["rule-providers"].clashoo_cn_ip.type == "file" and
			.["rule-providers"].clashoo_cn_ip.path == "./ruleset/cn-ip.txt" and
			.["rule-providers"].clashoo_cn_ip.format == "text" and
			.["rule-providers"].clashoo_cn_ip.behavior == "ipcidr" and
			([$doc | .. | select(tag == "!!str" and contains("clashoo_cn_ip")) |
				select(. != "RULE-SET,clashoo_cn_ip,DIRECT")] | length) == 0
		)) |= (
			del(.["rule-providers"].clashoo_cn_ip) |
			(select(has("rules")) | .rules) |= [.[] | select(. != "RULE-SET,clashoo_cn_ip,DIRECT")]
		)
	' "$CONFIG_YAML" 2>/dev/null; then
		log_msg "Failed to remove legacy CN fallback injection" "清理旧版国内直连注入失败，请检查配置"
	fi
}

_effective_fake_ip_filter_mode() {
	local mode
	mode="$(uci -q get clashoo.config.fake_ip_filter_mode 2>/dev/null)"
	if [ -z "$mode" ] && command -v yq >/dev/null 2>&1 && [ -s "$CONFIG_YAML" ]; then
		mode="$(yq -M e '(.dns."fake-ip-filter-mode" // "") | tostring' "$CONFIG_YAML" 2>/dev/null)"
		[ "$mode" = "null" ] && mode=""
	fi
	[ -n "$mode" ] || mode="blacklist"
	printf '%s\n' "$mode"
}

_shipped_fake_ip_filter_defaults() {
	local f="/usr/share/clashoo/clashoo.default"
	[ -s "$f" ] || return 0
	sed -n "s/^[[:space:]]*list[[:space:]]\{1,\}fake_ip_filter[[:space:]]\{1,\}'\{0,1\}\([^']*\)'\{0,1\}[[:space:]]*$/\1/p" "$f"
}

_fake_ip_filter_as_rule() {
	local f="$1"
	case "$f" in
		*,fake-ip|*,real-ip) printf '%s\n' "$f"; return ;;
	esac
	case "$f" in
		*,*)          printf '%s,real-ip\n' "$f" ;;
		geosite:*)    printf 'GEOSITE,%s,real-ip\n' "${f#geosite:}" ;;
		rule-set:*)   printf 'RULE-SET,%s,real-ip\n' "${f#rule-set:}" ;;
		'+.'*)        printf 'DOMAIN-SUFFIX,%s,real-ip\n' "${f#+.}" ;;
		*'*'*)        printf 'DOMAIN-WILDCARD,%s,real-ip\n' "$f" ;;
		*)            printf 'DOMAIN,%s,real-ip\n' "$f" ;;
	esac
}

_yq_apply_atomic() {
	local expr="$1" cand
	cand="${CONFIG_YAML}.cand.$$"
	cp "$CONFIG_YAML" "$cand" 2>/dev/null || return 1
	if ! yq -M e -i "$expr" "$cand" 2>/dev/null; then
		rm -f "$cand" 2>/dev/null
		return 1
	fi
	if ! yq -M e '.' "$cand" >/dev/null 2>&1; then
		rm -f "$cand" 2>/dev/null
		return 1
	fi
	mv -f "$cand" "$CONFIG_YAML" 2>/dev/null || { rm -f "$cand" 2>/dev/null; return 1; }
	return 0
}

normalize_fake_ip_filter() {
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0

	local mode dir work out item skip d
	mode="$(_effective_fake_ip_filter_mode)"

	if [ "$mode" = "blacklist" ] ||
		[ "$(yq -M e '(.dns // {}) | has("fake-ip-filter")' "$CONFIG_YAML" 2>/dev/null)" != "true" ]; then
		CLASHOO_FIF_MODE="$mode" _yq_apply_atomic '.dns."fake-ip-filter-mode" = strenv(CLASHOO_FIF_MODE)' ||
			log_msg "Failed to write fake-ip-filter-mode, subscription value kept" \
				"写入 fake-ip 过滤模式失败，已保留订阅自带的设置"
		return 0
	fi

	dir="$(mktemp -d /tmp/.clashoo_fif.XXXXXX 2>/dev/null)" || return 0
	work="$dir/in"
	out="$dir/out"
	yq -M e '.dns."fake-ip-filter"[]' "$CONFIG_YAML" 2>/dev/null > "$work" || { rm -rf "$dir"; return 0; }
	[ -s "$work" ] || { rm -rf "$dir"; return 0; }

	while IFS= read -r item; do
		[ -n "$item" ] || continue
		if [ "$mode" = "whitelist" ]; then
			skip=0
			set -f
			for d in $(_shipped_fake_ip_filter_defaults); do
				[ "$item" = "$d" ] && skip=1 && break
			done
			set +f
			[ "$skip" = "1" ] && continue
			printf '%s\n' "$item" >> "$out"
		else
			_fake_ip_filter_as_rule "$item" >> "$out"
		fi
	done < "$work"

	if [ -s "$out" ]; then
		CLASHOO_FIF_MODE="$mode" CLASHOO_FIF="$(cat "$out")" _yq_apply_atomic \
			'.dns."fake-ip-filter-mode" = strenv(CLASHOO_FIF_MODE) | .dns."fake-ip-filter" = (strenv(CLASHOO_FIF) | split("\n"))' ||
			log_msg "Failed to normalize fake-ip filter, subscription settings kept" \
				"规范化 fake-ip 过滤失败，已保留订阅自带的模式与列表"
	else
		CLASHOO_FIF_MODE="$mode" _yq_apply_atomic \
			'.dns."fake-ip-filter-mode" = strenv(CLASHOO_FIF_MODE) | del(.dns."fake-ip-filter")' ||
			log_msg "Failed to normalize fake-ip filter, subscription settings kept" \
				"规范化 fake-ip 过滤失败，已保留订阅自带的模式与列表"
		log_msg "Fake-IP allowlist is empty after dropping shipped defaults, add your own entries" \
			"白名单模式下过滤列表已空（随包默认项在此模式下语义相反已跳过），请自行填写需要 Fake-IP 的域名"
	fi
	rm -rf "$dir" 2>/dev/null
}

_ensure_cn_domain_rule_provider() {
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0
	yq -M e -i '
		.["rule-providers"].cn_domain = {
			"type": "file",
			"path": "./ruleset/cn.mrs",
			"format": "mrs",
			"behavior": "domain"
		}
	' "$CONFIG_YAML" 2>/dev/null || true
}

append_luci_fake_ip_filter_extras() {
	[ -n "${CLASHOO_KEEP_FAKEIP_FILTER:-}" ] || return 0
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0

	local item needs_cn_file
	needs_cn_file="/tmp/.clashoo_need_cn_rs.$$"
	rm -f "$needs_cn_file" 2>/dev/null
	_uci_fake_ip_filter_extras | while IFS= read -r item; do
		[ -n "$item" ] || continue
		[ "$item" = "rule-set:cn_domain" ] && : > "$needs_cn_file"
		CLASHOO_FAKEIP_FILTER_ITEM="$item" yq -M e -i '
			.dns."fake-ip-filter" = (((.dns."fake-ip-filter" // []) + [strenv(CLASHOO_FAKEIP_FILTER_ITEM)]) | unique)
		' "$CONFIG_YAML" 2>/dev/null || true
	done
	[ -f "$needs_cn_file" ] && _ensure_cn_domain_rule_provider
	rm -f "$needs_cn_file" 2>/dev/null
}

ensure_dns_cn_policy_fallback() {
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0
	[ "$(yq -M e '(.dns // {}) | has("nameserver-policy")' "$CONFIG_YAML" 2>/dev/null)" = "true" ] || return 0
	[ "$(yq -M e '((.dns."nameserver-policy" // {}) | type) == "!!map"' "$CONFIG_YAML" 2>/dev/null)" = "true" ] || return 0

	if [ "$(yq -M e '(.dns."nameserver-policy" // {}) | has("rule-set:cn_domain")' "$CONFIG_YAML" 2>/dev/null)" = "true" ]; then
		_ensure_cn_domain_rule_provider
		return 0
	fi

	if [ "$(yq -M e '(.dns."nameserver-policy" // {}) | has("geosite:cn")' "$CONFIG_YAML" 2>/dev/null)" = "true" ]; then
		yq -M e -i '.dns."nameserver-policy"."rule-set:cn_domain" = .dns."nameserver-policy"."geosite:cn"' "$CONFIG_YAML" 2>/dev/null || true
	else
		yq -M e -i '.dns."nameserver-policy"."rule-set:cn_domain" = ["https://223.5.5.5/dns-query", "https://doh.pub/dns-query"]' "$CONFIG_YAML" 2>/dev/null || true
	fi
	_ensure_cn_domain_rule_provider
}

_remap_dns_system_nameserver() {
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$CONFIG_YAML" ] || return 0
	yq -M e -i '(.dns | .. | select(. == "system")) |= "223.5.5.5"' "$CONFIG_YAML" 2>/dev/null || true
}

ip_rules() {
#===========================================================================================================================
	CLASHOO_CONFIG_YAML="$CONFIG_YAML" sh /usr/share/clashoo/runtime/iprules.sh 2>/dev/null
#===========================================================================================================================
}


smart_inject() {
#===========================================================================================================================
	local smart_auto smart_priority smart_asn smart_lgbm smart_collect smart_collect_size
	local smart_collect_rate smart_lgbm_auto smart_lgbm_interval smart_lgbm_url
	local prefer_asn_val lgbm_val collect_val collect_rate PRIORITY_TMP

	smart_auto=$(uci get clashoo.config.smart_auto_switch 2>/dev/null)

	# Remove any fields injected by a previous smart_inject run (idempotency)
	sed -i '/^    uselightgbm:/d; /^    collectdata:/d; /^    prefer-asn:/d; /^    sample-rate:/d' "$CONFIG_YAML" 2>/dev/null
	sed -i '/^    policy-priority:/d' "$CONFIG_YAML" 2>/dev/null
	sed -i '/^lgbm-auto-update:/d; /^lgbm-url:/d; /^lgbm-update-interval:/d' "$CONFIG_YAML" 2>/dev/null
	# revert previous smart injection: type:smart -> url-test
	if command -v yq >/dev/null 2>&1; then
		yq e -i '(.["proxy-groups"] // []) |= map(if .type == "smart" then .type = "url-test" else . end)' "$CONFIG_YAML" 2>/dev/null
	fi

	[ "${smart_auto:-0}" != "1" ] && return 0
	# only inject smart fields when core=1; type:smart rejected by standard mihomo.
	[ "$(uci -q get clashoo.config.core 2>/dev/null)" = "1" ] || return 0

	smart_priority=$(uci get clashoo.config.smart_policy_priority 2>/dev/null)
	smart_asn=$(uci get clashoo.config.smart_prefer_asn 2>/dev/null)
	smart_lgbm=$(uci get clashoo.config.smart_uselightgbm 2>/dev/null)
	smart_collect=$(uci get clashoo.config.smart_collectdata 2>/dev/null)
	smart_collect_size=$(uci get clashoo.config.smart_collect_size 2>/dev/null)
	smart_collect_rate=$(uci get clashoo.config.smart_collect_rate 2>/dev/null)
	smart_lgbm_auto=$(uci get clashoo.config.smart_lgbm_auto_update 2>/dev/null)
	smart_lgbm_interval=$(uci get clashoo.config.smart_lgbm_update_interval 2>/dev/null)
	smart_lgbm_url=$(uci get clashoo.config.smart_lgbm_url 2>/dev/null)

	[ -z "$smart_collect_size" ] && smart_collect_size=100
	[ -z "$smart_collect_rate" ] && smart_collect_rate=1
	[ -z "$smart_lgbm_interval" ] && smart_lgbm_interval=72
	[ -z "$smart_lgbm_url" ] && smart_lgbm_url="https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin"

	[ "${smart_asn:-0}" = "1" ] && prefer_asn_val="true" || prefer_asn_val="false"
	[ "${smart_lgbm:-0}" = "1" ] && lgbm_val="true" || lgbm_val="false"
	[ "${smart_collect:-0}" = "1" ] && collect_val="true" || collect_val="false"
	collect_rate="${smart_collect_rate:-1}"

	# Prefer structured YAML mutation (OpenClash-like behavior) when yq exists.
	if command -v yq >/dev/null 2>&1; then
		export SMART_PRIORITY="${smart_priority:-}"
		export SMART_PREFER_ASN="$prefer_asn_val"
		export SMART_USELGBM="$lgbm_val"
		export SMART_COLLECT="$collect_val"
		export SMART_COLLECT_RATE="${collect_rate:-1}"
		export SMART_COLLECT_SIZE="${smart_collect_size:-100}"
		export SMART_LGBM_AUTO="$([ "${smart_lgbm_auto:-0}" = "1" ] && echo "true" || echo "false")"
		export SMART_LGBM_URL="${smart_lgbm_url:-}"
		export SMART_LGBM_INTERVAL="${smart_lgbm_interval:-72}"

		if yq e '
			.["proxy-groups"] = ((.["proxy-groups"] // []) | map(
				if (.type == "url-test" or .type == "load-balance" or .type == "smart") then
					.type = "smart" |
					.["prefer-asn"] = (env(SMART_PREFER_ASN) == "true") |
					.uselightgbm = (env(SMART_USELGBM) == "true") |
					.collectdata = (env(SMART_COLLECT) == "true") |
					.["sample-rate"] = (env(SMART_COLLECT_RATE) | tonumber) |
					(if env(SMART_PRIORITY) != "" then .["policy-priority"] = env(SMART_PRIORITY) else del(.["policy-priority"]) end)
			else .
				end
			)) |
			(if env(SMART_COLLECT) == "true" then
				.profile = (.profile // {}) |
				.profile["smart-collector-size"] = (env(SMART_COLLECT_SIZE) | tonumber)
			else
				.
			end) |
			(if env(SMART_USELGBM) == "true" then
				.["lgbm-auto-update"] = (env(SMART_LGBM_AUTO) == "true") |
				.["lgbm-url"] = env(SMART_LGBM_URL) |
				.["lgbm-update-interval"] = (env(SMART_LGBM_INTERVAL) | tonumber)
			else
				del(.["lgbm-auto-update"], .["lgbm-url"], .["lgbm-update-interval"])
			end)
		' "$CONFIG_YAML" > /tmp/_smart_inject.yaml 2>/dev/null; then
			mv /tmp/_smart_inject.yaml "$CONFIG_YAML"
			unset SMART_PRIORITY SMART_PREFER_ASN SMART_USELGBM SMART_COLLECT SMART_COLLECT_RATE
			unset SMART_COLLECT_SIZE SMART_LGBM_AUTO SMART_LGBM_URL SMART_LGBM_INTERVAL
			return 0
		fi

		unset SMART_PRIORITY SMART_PREFER_ASN SMART_USELGBM SMART_COLLECT SMART_COLLECT_RATE
		unset SMART_COLLECT_SIZE SMART_LGBM_AUTO SMART_LGBM_URL SMART_LGBM_INTERVAL
	fi

	# Fallback path (no yq): line-based mutation with indentation-aware awk.
	# Step 1: Replace any-indentation type: url-test/load-balance to type: smart
	awk '
	/^[ \t]*type:[ \t]*(url-test|load-balance)[ \t]*$/ {
		sub(/type:[ \t]*(url-test|load-balance)[ \t]*$/, "type: smart")
	}
	{ print }
	' "$CONFIG_YAML" > /tmp/_smart_type.yaml && mv /tmp/_smart_type.yaml "$CONFIG_YAML"

	# Step 2: Build policy-priority value temp file (avoid quoting issues)
	PRIORITY_TMP="/tmp/_smart_priority.txt"
	printf '' > "$PRIORITY_TMP"
	if [ -n "$smart_priority" ]; then
		printf '%s\n' "$smart_priority" > "$PRIORITY_TMP"
	fi

	# Step 3: Inject smart fields after each "type: smart" with preserved indentation
	awk -v pfile="$PRIORITY_TMP" \
	    -v prefer_asn="$prefer_asn_val" \
	    -v lgbm="$lgbm_val" \
	    -v collect="$collect_val" \
	    -v rate="$collect_rate" '
	/^[ \t]*type:[ \t]*smart[ \t]*$/ {
	    match($0, /^[ \t]*/)
	    indent = substr($0, RSTART, RLENGTH)
	    print
	    while ((getline line < pfile) > 0) {
	        if (line != "")
	            print indent "policy-priority: \047" line "\047"
	    }
	    close(pfile)
	    print indent "prefer-asn: " prefer_asn
	    print indent "uselightgbm: " lgbm
	    print indent "collectdata: " collect
	    print indent "sample-rate: " rate
	    next
	}
	{ print }
	' "$CONFIG_YAML" > /tmp/_smart_inject.yaml && mv /tmp/_smart_inject.yaml "$CONFIG_YAML"
	rm -f "$PRIORITY_TMP" 2>/dev/null

	# Step 4: profile.smart-collector-size (preserve profile child indentation from source YAML)
	if grep -q '^profile:[ 	]*$' "$CONFIG_YAML"; then
		profile_indent="$(
			awk '
			/^profile:[ \t]*$/ {
				while (getline > 0) {
					if ($0 ~ /^[ \t]*$/) {
						continue
					}
					if ($0 ~ /^[^ \t]/) {
						break
					}
					match($0, /^[ \t]+/)
					print substr($0, RSTART, RLENGTH)
					exit
				}
			}
			' "$CONFIG_YAML"
		)"
		[ -n "$profile_indent" ] || profile_indent="  "

		if grep -q '^[ 	]*smart-collector-size:[ 	]*' "$CONFIG_YAML"; then
			awk -v indent="$profile_indent" -v size="$smart_collect_size" '
			/^[ \t]*smart-collector-size:[ \t]*/ {
				print indent "smart-collector-size: " size
				next
			}
			{ print }
			' "$CONFIG_YAML" > /tmp/_smart_profile.yaml && mv /tmp/_smart_profile.yaml "$CONFIG_YAML"
		else
			awk -v indent="$profile_indent" -v size="$smart_collect_size" '
			/^profile:[ \t]*$/ {
				print
				print indent "smart-collector-size: " size
				next
			}
			{ print }
			' "$CONFIG_YAML" > /tmp/_smart_profile.yaml && mv /tmp/_smart_profile.yaml "$CONFIG_YAML"
		fi
	else
		printf '\nprofile:\n  store-selected: true\n  smart-collector-size: %s\n' "$smart_collect_size" >> "$CONFIG_YAML"
	fi

	# Step 5: Top-level lgbm fields (only when lgbm enabled)
	if [ "$lgbm_val" = "true" ]; then
		lgbm_auto_val="false"
		[ "${smart_lgbm_auto:-0}" = "1" ] && lgbm_auto_val="true"
		printf 'lgbm-auto-update: %s\nlgbm-url: "%s"\nlgbm-update-interval: %s\n' \
		    "$lgbm_auto_val" "$smart_lgbm_url" "$smart_lgbm_interval" >> "$CONFIG_YAML"
	fi
#===========================================================================================================================
}


_lan_acl_enabled() {
	local section
	for section in $(uci -q show clashoo 2>/dev/null | sed -n 's/^clashoo\.\([^.=]*\)=lan_acl$/\1/p'); do
		[ "$(uci -q get "clashoo.${section}.enabled" 2>/dev/null)" != "0" ] && return 0
	done
	[ "$(uci -q get clashoo.config.acl_migrated 2>/dev/null)" = "1" ] && return 1
	case "$(uci -q get clashoo.config.access_control 2>/dev/null)" in
		1|2) return 0 ;;
	esac
	return 1
}

_lan_dns_split_enabled() {
	_lan_acl_enabled || return 1
	[ "$(uci -q get clashoo.config.enable_dns 2>/dev/null)" != "0" ] || return 1
	[ "$(uci -q get clashoo.config.ipv4_dns_hijack 2>/dev/null)" = "1" ] ||
		[ "$(uci -q get clashoo.config.ipv6_dns_hijack 2>/dev/null)" = "1" ]
}

yml_dns_change(){
#=========================================================================================================================== 	
	dns_port=$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' "$CONFIG_YAML" 2>/dev/null | head -n1)
	listen_port=$(uci get clashoo.config.listen_port 2>/dev/null)
	[ -z "$listen_port" ] && listen_port=5300
	if [ -z "$dns_port" ]; then
		dns_port="$listen_port"
		if grep -q '^dns:' "$CONFIG_YAML" && ! grep -q "^ \{0,\}listen:" "$CONFIG_YAML"; then
			sed -i "/^dns:/a\    listen: 0.0.0.0:${dns_port}" "$CONFIG_YAML" 2>/dev/null
		fi
	fi
	if [ "${CLASHOO_RELOAD_CANDIDATE:-0}" = "1" ]; then
		if [ "${dns_port:-0}" -eq 53 ]; then
			sed -i "s@^\([[:space:]]*listen:[[:space:]]*\)0\.0\.0\.0:53@\10.0.0.0:${listen_port}@g" "$CONFIG_YAML" || return 1
		fi
		return 0
	fi
	dnsforwader=$(uci get clashoo.config.dnsforwader 2>/dev/null)
	dnscache=$(uci get clashoo.config.dnscache 2>/dev/null)
	if [ "$(uci -q get clashoo.config.enable_dns 2>/dev/null)" = "0" ]; then
		revert_dns >/dev/null 2>&1
		return 0
	fi
	if _lan_dns_split_enabled; then
		revert_dns 1 >/dev/null 2>&1
		return 0
	fi
	backup_dnsmasq_state
	if [ "${dns_port:-0}" -eq 53 ]; then
			sed -i "s@^\([[:space:]]*listen:[[:space:]]*\)0\.0\.0\.0:53@\10.0.0.0:${listen_port}@g" "$CONFIG_YAML"
			dns_port="$listen_port"
	fi	

	if [ "${dnsforwader:-0}" -ne 0 ]; then	
		   uci -q delete dhcp.@dnsmasq[0].server
		   if [ "${dns_port:-0}" -eq 53 ]; then
	   
			log_msg "Setting dns forwarder" "设置dns转发器"

			  add_clash_dns_server 5300
	          uci delete dhcp.@dnsmasq[0].resolvfile
	          uci set dhcp.@dnsmasq[0].noresolv=1
			  uci commit dhcp
		  if [ "${dnscache:-0}" -eq 0 ];then
			log_msg "Disabling dns cache" "禁用dns缓存"
			uci set dhcp.@dnsmasq[0].cachesize=0
			uci commit dhcp
		  fi  
	    else
		  echo "Setting dns forwarder" >$REAL_LOG
			  add_clash_dns_server "$dns_port"
	          uci delete dhcp.@dnsmasq[0].resolvfile
	          uci set dhcp.@dnsmasq[0].noresolv=1
			  uci commit dhcp
		  if [ "${dnscache:-0}"  -eq 0 ];then
			log_msg "Disabling dns cache" "禁用dns缓存"

	        uci set dhcp.@dnsmasq[0].cachesize=0
			uci commit dhcp
		  fi	      
	   fi
	elif [ "${dnsforwader:-0}" -ne 1 ]; then
		if [ "${dnscache:-0}"  -eq 0 ];then
			log_msg "Disabling dns cache" "禁用dns缓存"

	        uci set dhcp.@dnsmasq[0].cachesize=0
			uci commit dhcp
		fi
	fi
#=========================================================================================================================== 	   
}

singbox_dns_change() {
#===========================================================================================================================
	local dnsforwader dnscache dns_port
	if _lan_dns_split_enabled; then
		revert_dns 1 >/dev/null 2>&1
		return 0
	fi
	dnsforwader="$(uci get clashoo.config.dnsforwader 2>/dev/null)"
	if [ "${dnsforwader:-0}" -eq 0 ]; then
		return 0
	fi

	dnscache="$(uci get clashoo.config.dnscache 2>/dev/null)"
	dns_port="$(uci get clashoo.config.listen_port 2>/dev/null)"
	[ -z "$dns_port" ] && dns_port=1053

	backup_dnsmasq_state
	_sb_dns_before="$(_dnsmasq_state_digest)"
	uci -q delete dhcp.@dnsmasq[0].server
	add_clash_dns_server "$dns_port"
	uci -q delete dhcp.@dnsmasq[0].resolvfile
	uci set dhcp.@dnsmasq[0].noresolv='1'
	if [ "${dnscache:-0}" -eq 0 ]; then
		uci set dhcp.@dnsmasq[0].cachesize='0'
	fi
	if [ "$(_dnsmasq_state_digest)" != "$_sb_dns_before" ]; then
		uci commit dhcp >/dev/null 2>&1
		_reload_dnsmasq >/dev/null 2>&1
	else
		uci -q revert dhcp >/dev/null 2>&1
	fi
	unset _sb_dns_before
#===========================================================================================================================
}


check(){

	   sed -i 's/^Proxy Group:/proxy-groups:/g' "$CONFIG_YAML"
	   sed -i 's/^proxy-provider:/proxy-providers:/g' "$CONFIG_YAML"
	   sed -i 's/^Proxy:/proxies:/g' "$CONFIG_YAML"
	   sed -i 's/^Rule:/rules:/g' "$CONFIG_YAML"
	   sed -i 's/^rule-provider:/rule-providers:/g' "$CONFIG_YAML"

}

rules(){
#===========================================================================================================================
		/usr/share/clashoo/net/fw4.sh apply >/dev/null 2>&1
		return $?
#=========================================================================================================================== 
}	 


restore() {
	sh /usr/share/clashoo/runtime/restore.sh >/dev/null 2>&1
}

_config_needs_mmdb() {
	grep -Eq '^[[:space:]]*-[[:space:]]*["'\'']?GEOIP,|^[[:space:]]*geoip:[[:space:]]|^[[:space:]]*geoip-code:[[:space:]]' "$CONFIG_YAML"
}

_has_valid_local_mmdb() {
	local db_file db_size

	for db_file in /etc/clashoo/geoip.metadb /etc/clashoo/Country.mmdb; do
		[ -f "$db_file" ] || continue
		db_size=$(wc -c <"$db_file" 2>/dev/null)
		[ -n "$db_size" ] || continue
		if [ "$db_file" = "/etc/clashoo/geoip.metadb" ] && [ "$db_size" -ge 1000000 ]; then
			return 0
		fi
		if [ "$db_file" = "/etc/clashoo/Country.mmdb" ] && [ "$db_size" -ge 2000000 ]; then
			return 0
		fi
	done

	return 1
}

_config_needs_geosite() {
	# Matches: routing rules (- GEOSITE,...), geosite: child blocks, and
	# list-item refs like `- geosite:cn` / `- 'geosite:cn'` used in
	# fake-ip-filter (the default config ships one, easy to miss).
	grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*[\"']geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$CONFIG_YAML"
}

_has_valid_local_geosite() {
	local site_file site_size

	for site_file in /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat; do
		[ -f "$site_file" ] || continue
		site_size=$(wc -c <"$site_file" 2>/dev/null)
		[ -n "$site_size" ] || continue
		if [ "$site_size" -ge 2000000 ]; then
			if [ "$site_file" = "/etc/clashoo/geosite.dat" ]; then
				cp -f "$site_file" /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || true
				chmod 644 /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || true
			fi
			return 0
		fi
	done

	return 1
}

_strip_geox_url_block() {
	awk '
	BEGIN { skip=0 }
	/^geox-url:[[:space:]]*$/ { skip=1; next }
	skip && /^[^[:space:]]/ { skip=0 }
	!skip { print }
	' "$CONFIG_YAML" > /tmp/_clashoo_geox.yaml && mv /tmp/_clashoo_geox.yaml "$CONFIG_YAML"
}

_strip_yaml_child_block() {
	local regex="$1"
	awk -v target="$regex" '
	function indent_of(line) {
		match(line, /^[[:space:]]*/)
		return RLENGTH
	}
	{
		if (skip) {
			if (length($0) == 0) {
				next
			}
			if (indent_of($0) > base_indent) {
				next
			}
			skip=0
		}
		if ($0 ~ target) {
			skip=1
			base_indent=indent_of($0)
			next
		}
		print
	}
	' "$CONFIG_YAML" > /tmp/_clashoo_geo_block.yaml && mv /tmp/_clashoo_geo_block.yaml "$CONFIG_YAML"
}

_strip_yaml_rule_lines() {
	local regex="$1"
	awk -v target="$regex" '
	$0 ~ target { next }
	{ print }
	' "$CONFIG_YAML" > /tmp/_clashoo_geo_rules.yaml && mv /tmp/_clashoo_geo_rules.yaml "$CONFIG_YAML"
}


_start_geoip_update_async() {
	if [ -f /var/run/geoip_update ]; then
		return 0
	fi
	touch /var/run/geoip_update >/dev/null 2>&1
	(exec 1000>&-; nohup /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 </dev/null &)
}

_prepare_geo_bootstrap_mode() {
	local needs_mmdb needs_geosite

	needs_mmdb=0
	needs_geosite=0

	_config_needs_mmdb && needs_mmdb=1
	_config_needs_geosite && needs_geosite=1

	if [ "$needs_mmdb" -eq 1 ] && _has_valid_local_mmdb; then
		needs_mmdb=0
	elif [ "$needs_mmdb" -eq 1 ] && { [ -f /etc/clashoo/Country.mmdb ] || [ -f /etc/clashoo/geoip.metadb ]; }; then
		rm -f /etc/clashoo/Country.mmdb >/dev/null 2>&1
		rm -f /etc/clashoo/geoip.metadb >/dev/null 2>&1
		log_msg "GeoIP database is invalid or incomplete, fallback to bootstrap mode" "GeoIP 数据库无效或下载不完整，已回退到引导模式"
	fi
	[ "$needs_geosite" -eq 1 ] && _has_valid_local_geosite && needs_geosite=0

	if [ "$needs_mmdb" -eq 0 ] && [ "$needs_geosite" -eq 0 ]; then
		return 0
	fi

	sed -i '/^geodata-mode:/d; /^geodata-loader:/d' "$CONFIG_YAML" 2>/dev/null
	_strip_geox_url_block

	if [ "$needs_mmdb" -eq 1 ]; then
		_strip_yaml_rule_lines "^[[:space:]]*-[[:space:]]*[\"']?GEOIP,"
		sed -i '/^[[:space:]]*geoip:[[:space:]]*/d' "$CONFIG_YAML" 2>/dev/null
		sed -i '/^[[:space:]]*geoip-code:[[:space:]]*/d' "$CONFIG_YAML" 2>/dev/null
	fi

	if [ "$needs_geosite" -eq 1 ]; then
		rm -f /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1
		_strip_yaml_rule_lines "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,"
		_strip_yaml_child_block '^[[:space:]]*geosite:[[:space:]]*$'
		_strip_yaml_child_block "^[[:space:]]*'geosite:[^']*'[[:space:]]*:[[:space:]]*$"
		_strip_yaml_child_block '^[[:space:]]*"geosite:[^"]*"[[:space:]]*:[[:space:]]*$'
		_strip_yaml_child_block '^[[:space:]]*geosite:[^[:space:]]+:[[:space:]]*$'
		_strip_yaml_rule_lines '^[[:space:]]*geosite:[[:space:]]+[^[:space:]].*'
		# list-item refs (e.g. fake-ip-filter: - geosite:cn / - 'geosite:cn')
		sed -i "/^[[:space:]]*-[[:space:]]*[\"']\{0,1\}geosite:/d" "$CONFIG_YAML" 2>/dev/null
	fi

	# Defer the geo download until the core is up: a fresh box has no working
	# proxy yet, so fetching now would hit the (likely blocked) mirrors and fail.
	# Record what was missing so the post-start watcher can fetch geo through the
	# live proxy and only reload once geo is complete. See _finish_geo_bootstrap.
	printf '%s %s\n' "$needs_mmdb" "$needs_geosite" > /var/run/clashoo_bootstrap_pending 2>/dev/null
	log_msg "Geo data missing, started in bootstrap mode; will fetch geo via proxy then reload" "Geo 数据缺失，已切换为引导模式；待代理可用后自动补全并重载"
}

# After the core is up in bootstrap mode, fetch the missing geo data through the
# now-working proxy and, once it is complete, reload the full (un-stripped)
# config so geo rules take effect. Loop-safe: only reloads when the geo that was
# actually missing is now valid, so the next start won't re-enter bootstrap.
_finish_geo_bootstrap() {
	local need_mmdb need_geosite still
	[ -f /var/run/clashoo_bootstrap_pending ] || return 0
	# core-only never strips, so a flag there would be stale — clear and skip.
	if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ]; then
		rm -f /var/run/clashoo_bootstrap_pending >/dev/null 2>&1
		return 0
	fi
	read need_mmdb need_geosite < /var/run/clashoo_bootstrap_pending 2>/dev/null

	log_msg "Bootstrap: fetching geo data via proxy" "引导模式：正在通过代理补全 Geo 数据"
	if [ "$need_geosite" = "1" ]; then
		/usr/share/clashoo/update/geoip.sh --require-geosite >/dev/null 2>&1
	else
		/usr/share/clashoo/update/geoip.sh >/dev/null 2>&1
	fi

	still=0
	[ "$need_mmdb" = "1" ] && ! _has_valid_local_mmdb && still=1
	[ "$need_geosite" = "1" ] && ! _has_valid_local_geosite && still=1
	if [ "$still" = "1" ]; then
		log_msg "Bootstrap: geo fetch incomplete, will retry on next update" "引导模式：Geo 补全未完成，将在下次更新时重试"
		return 0
	fi

	rm -f /var/run/clashoo_bootstrap_pending >/dev/null 2>&1
	_clashoo_enabled || return 0
	log_msg "Bootstrap: geo ready, reloading full config" "引导模式：Geo 数据已就绪，正在重载完整配置"
	/etc/init.d/clashoo restart >/dev/null 2>&1
}





_start_validate_core() {
	local core bin
	core="$(uci -q get clashoo.config.core 2>/dev/null)"
	if is_singbox_selected; then
		bin="$(selected_singbox_binary)"
		if [ -z "$bin" ] || ! _ensure_singbox_binary; then
			log_msg "sing-box core not found, please install the local sing-box binary first" "找不到本地 sing-box 内核，请先安装 sing-box 二进制"
			echo "Clashoo" > "$REAL_LOG"
			return 1
		fi
		return 0
	fi

	if [ "${core:-0}" -eq 1 ] && [ ! -x "$CLASH" ]; then
		log_msg "Smart core not found, please install the local Smart binary first" "找不到本地 Smart 内核，请先安装 Smart 二进制"
		echo "Clashoo" > "$REAL_LOG"
		return 1
	elif [ "${core:-0}" -eq 2 ] && [ ! -x "$CLASH" ]; then
		log_msg "mihomo stable core not found, detected binary is unavailable: $CLASH" "找不到可用的 mihomo 稳定版内核: $CLASH"
		sleep 1
		echo "Clashoo" > "$REAL_LOG"
		return 1
	elif [ "${core:-0}" -eq 3 ] && [ ! -x "$CLASH" ]; then
		log_msg "mihomo Alpha core not found, detected binary is unavailable: $CLASH" "找不到可用的 mihomo Alpha 内核，请在组件更新中安装: $CLASH"
		sleep 1
		echo "Clashoo" > "$REAL_LOG"
		return 1
	elif [ "${core:-0}" -ne 1 ] && [ "${core:-0}" -ne 2 ] && [ "${core:-0}" -ne 3 ] && [ ! -x "$CLASH" ]; then
		log_msg "invalid core setting (${core}), detected binary is unavailable: $CLASH" "内核配置异常 (${core})，且找不到可用内核: $CLASH"
		echo "Clashoo" > "$REAL_LOG"
		return 1
	fi
	return 0
}

_start_prepare_config() {
	local _reload_candidate="${CLASHOO_RELOAD_CANDIDATE:-0}"
	# Core-only: run the imported config verbatim. select_config already copied
	# it to /etc/clashoo/config.yaml, so skip every mixin/DNS/rule rewrite below.
	[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 0

	check >/dev/null 2>&1 || return 1

	[ "$_reload_candidate" = "1" ] || _migrate_dns_roles >/dev/null 2>&1

	enable_dns=$(uci get clashoo.config.enable_dns 2>/dev/null)

	if [ -z "$(grep -E "^ {0,}listen:" "$CONFIG_YAML")" ] || [ -z "$(grep -E "^ {0,}enhanced-mode:" "$CONFIG_YAML")" ] || [ -z "$(grep -E "^ {0,}dns:" "$CONFIG_YAML")" ];then

	if [ "${enable_dns:-0}" -eq 0 ];then
		[ "$_reload_candidate" = "1" ] && return 1
			uci set clashoo.config.enable_dns="1" && uci commit clashoo
			log_msg "Enabling Custom Dns" "启用自定义DNS"
	fi

	fi

	if { egrep -q '^ {0,}rule-providers:' "$CONFIG_YAML" || egrep -q '^ {0,}script:' "$CONFIG_YAML"; } && ! core_supports_advanced_features; then
		[ "$_reload_candidate" = "1" ] && return 1
		if [ -n "$(_mihomo_stable_binary)" ]; then
			uci set clashoo.config.core="2" && uci commit clashoo
		elif [ -x /usr/share/clashoo/bin/mihomo-alpha ]; then
			uci set clashoo.config.core="3" && uci commit clashoo
		else
			log_msg "Current config requires mihomo/clash-meta core" "当前配置需要 mihomo 或 clash-meta 内核"
			backend_remove_rules >/dev/null 2>&1
			sleep 1
			echo "Clashoo" > "$REAL_LOG"
			exit 0
		fi
		core=$(uci get clashoo.config.core 2>/dev/null)
		CLASH="$(_detect_selected_core)"
		sleep 1
	fi

	if [ "$(uci get clashoo.config.p_mode 2>/dev/null)" = "script" ]; then
		[ "$_reload_candidate" = "1" ] && return 1
		uci set clashoo.config.p_mode="rule" && uci commit clashoo
	fi

	# flatten YAML merge anchors first (yq 4.53+ breaks "<<" keys on rewrite)
	if command -v yq >/dev/null 2>&1 && grep -q '<<[[:space:]]*:' "$CONFIG_YAML" 2>/dev/null; then
		yq e -i 'explode(.)' "$CONFIG_YAML" 2>/dev/null || true
	fi
	_adapt_dns_loopback_resolver >/dev/null 2>&1
	# stage bundled cn.mrs into the core dir; mihomo rejects rule-set paths outside /etc/clashoo
	if [ "$_reload_candidate" != "1" ] && [ -f /usr/share/clashoo/ruleset/cn.mrs ]; then
		if [ ! -f /etc/clashoo/ruleset/cn.mrs ] || [ /usr/share/clashoo/ruleset/cn.mrs -nt /etc/clashoo/ruleset/cn.mrs ]; then
			mkdir -p /etc/clashoo/ruleset
			cp -f /usr/share/clashoo/ruleset/cn.mrs /etc/clashoo/ruleset/cn.mrs 2>/dev/null
		fi
	fi
	_dns_node_policy_capture
	yml_change >/dev/null 2>&1
	_dns_node_policy_apply
	append_luci_fake_ip_filter_extras >/dev/null 2>&1
	normalize_fake_ip_filter >/dev/null 2>&1
	drop_legacy_cn_ip_injection >/dev/null 2>&1
	dns_leak_protect=$(uci -q get clashoo.config.dns_leak_protect 2>/dev/null)
	if command -v dns_mihomo_apply_leak_protect >/dev/null 2>&1; then
		case "$(uci -q get clashoo.config.ipv6_proxy 2>/dev/null)" in
			1|true) _ipv6_proxied=1 ;;
			*) _ipv6_proxied=0 ;;
		esac
		if [ "${dns_leak_protect:-0}" = "1" ] && [ "$_ipv6_proxied" = "0" ]; then
			_ipv6_value="false"
		else
			case "$(uci -q get clashoo.config.enable_ipv6 2>/dev/null)" in
				true|1) _ipv6_value="true" ;;
				false|0) _ipv6_value="false" ;;
				*) _ipv6_value="" ;;
			esac
		fi
		unset _ipv6_proxied
		dns_mihomo_apply_leak_protect "$CONFIG_YAML" "${dns_leak_protect:-0}" "$_ipv6_value" >/dev/null 2>&1
		unset _ipv6_value
	fi
	smart_inject >/dev/null 2>&1

	if ! _has_valid_local_mmdb; then
		log_msg "GeoIP database not found, continue without local GeoIP DB" "未找到本地 GeoIP 数据库，继续启动（可在系统页更新 GeoIP）"
	fi

	[ "$_reload_candidate" = "1" ] || _prepare_geo_bootstrap_mode
	_remap_dns_system_nameserver >/dev/null 2>&1
	ensure_dns_cn_policy_fallback >/dev/null 2>&1


	if [ ! -f "$CONFIG_YAML" ];then
		[ "$_reload_candidate" = "1" ] && return 1
		log_msg "Cannot find config.yaml" "找不到config.yaml"
		backend_remove_rules >/dev/null 2>&1
		sleep 1
		echo "Clashoo" > "$REAL_LOG"
		exit 0
	fi

	yml_dns_change >/dev/null 2>&1 || return 1

	# Sync processed config (with DNS listen injected) to mihomo working directory
	[ "$_reload_candidate" = "1" ] || cp -f "$CONFIG_YAML" "${CLASH_CONFIG}/config.yaml" 2>/dev/null

	ip_rules >/dev/null 2>&1
}

_core_only_ui_args() {
	[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] || return 0
	[ -f /etc/clashoo/dashboard/index.html ] || return 0
	grep -Eq '^[[:space:]]*external-ui:' "$CONFIG_YAML" 2>/dev/null && return 0
	printf ' -ext-ui %s' "$(shell_quote /etc/clashoo/dashboard)"
}

_mihomo_proxy_dns_valid() {
	local config="$1" proxy_count policy_count respect_rules
	command -v yq >/dev/null 2>&1 || return 0
	[ -s "$config" ] || return 0
	proxy_count="$(yq -M e -r '(.dns."proxy-server-nameserver" // []) | length' "$config" 2>/dev/null)"
	[ "$proxy_count" = "0" ] || return 0
	policy_count="$(yq -M e -r '(.dns."proxy-server-nameserver-policy" // {}) | length' "$config" 2>/dev/null)"
	respect_rules="$(yq -M e -r '.dns."respect-rules" // false' "$config" 2>/dev/null)"
	[ "$policy_count" = "0" ] && [ "$respect_rules" != "true" ]
}

_start_launch_core() {
	local _cmd _core_only
	disable_quic_gso=$(uci get clashoo.config.disable_quic_gso 2>/dev/null)
	_core_only=$(uci -q get clashoo.config.core_only 2>/dev/null)
	mkdir -p "$(dirname "$CORE_LOG_PATH")" >/dev/null 2>&1
	touch "$CORE_LOG_PATH" >/dev/null 2>&1
	# core.log 在 tmpfs 上，先按大小裁剪，避免上次跑飞的大日志直接卡死启动
	sh /usr/share/clashoo/lib/core_log_guard.sh "$CORE_LOG_PATH" >/dev/null 2>&1

	# Core-only runs the imported config verbatim, which may reference absolute
	# paths (e.g. OpenClash's external-ui /usr/share/openclash/ui). mihomo's
	# SAFE_PATHS guard rejects paths outside the working dir, so relax it here —
	# in core-only the user is explicitly trusting their own config.
	[ "$_core_only" = "1" ] && export SAFE_PATHS="/"

	if ! _mihomo_proxy_dns_valid "${CLASH_CONFIG}/config.yaml"; then
		log_msg "Proxy node DNS is required by node DNS policy or respect-rules; set a Proxy Node Domain Resolver in LuCI" "节点域名 DNS 策略或 DNS 遵循规则需要代理节点域名解析，请在 LuCI 中配置"
		runtime_state_record_health "fail" "preflight:proxy_dns_missing"
		echo "Clashoo" > "$REAL_LOG"
		return 1
	fi

	# preflight dry-run with watchdog: bail on bad config without touching
	# firewall/dnsmasq, and never let a geo auto-download (mihomo blocks ~90s on
	# a missing GeoIP/GeoSite when GitHub is unreachable) freeze startup/UI.
	_run_with_timeout 15 "$CLASH" -d "$CLASH_CONFIG" -t >> "$CORE_LOG_PATH" 2>&1
	_pf_rc=$?
	if [ "$_pf_rc" -ne 0 ]; then
		_pf_reason="$(_preflight_reason)"
		if [ "$_pf_rc" -eq 124 ]; then
			log_msg "Config preflight timed out (likely downloading geo data), retry shortly. ${_pf_reason}" "配置预检超时（可能正在下载 Geo 数据），稍后自动重试。${_pf_reason}"
			runtime_state_record_health "fail" "preflight:timeout"
			_start_geoip_update_async
		else
			log_msg "Config dry-run failed; firewall/DNS untouched. ${_pf_reason}" "配置预检失败，未应用防火墙与 DNS 改动。${_pf_reason}"
			runtime_state_record_health "fail" "preflight:config_dry_run_failed"
		fi
		echo "Clashoo" > "$REAL_LOG"
		return 1
	fi

	procd_open_instance
	_cmd="$(shell_quote "$CLASH") -d $(shell_quote "$CLASH_CONFIG")$(_core_only_ui_args) >> $(shell_quote "$CORE_LOG_PATH") 2>&1"
	procd_set_param command /bin/sh -c "$_cmd"
	[ "$disable_quic_gso" = "1" ] && procd_set_param env QUIC_GO_DISABLE_GSO=1
	[ "$_core_only" = "1" ] && procd_set_param env SAFE_PATHS=/
	# 默认 4095 个 fd 顶不住 BT/PCDN 类连接洪水，打爆后核心会陷入
	# accept 失败刷屏，与 sing-box 实例保持一致提到 1000000。
	procd_set_param limits core="unlimited"
	procd_set_param limits nofile="1000000 1000000"
	procd_set_param respawn 3600 5 5
	procd_close_instance
	[ "$disable_quic_gso" = "1" ] && log_msg "Disable quic-go GSO support" "已启用：禁用 quic-go GSO 支持"
	ln -s /usr/share/clashoo/yacd /www/luci-static/yacd 2>/dev/null

	# Core-only: just keep the core process; do not take over firewall/DNS/cron.
	if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then
		log_msg "Configuring firewall rules" "配置防火墙规则"
		backend_apply_rules >/dev/null 2>&1

		if [ "$(_dnsmasq_state_digest)" != "$_dnsmasq_digest_before" ]; then
			log_msg "Reloading DNS resolver" "重启 DNS 解析"
			_reload_dnsmasq >/dev/null 2>&1
		fi

		restore >/dev/null 2>&1
		add_cron
	else
		log_msg "Core-only mode: skipping firewall/DNS takeover" "仅内核模式：跳过防火墙与 DNS 接管"
	fi
	echo "Clashoo" > "$REAL_LOG"
	# procd starts the core process only after start_service() returns, so we
	# cannot block here waiting for it. Launch a background watcher instead.
	# If the core never appears within the window, roll back the firewall
	# rules and dnsmasq override we already applied — otherwise the box is
	# left with a hijacked DNS pointing at a port nobody is listening on.
	(
		exec 1000>&-
		_bg_max="$(_compute_start_wait)"
		bg_wait=0
		while [ "$bg_wait" -lt "$_bg_max" ]; do
			sleep 1
			if _core_running; then
				runtime_run_health_check_settle >/dev/null 2>&1
				# kick access check immediately so panel shows current state (daemon polls every 5-20s)
				/usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1 &
				# bootstrap mode: now that the proxy is up, fetch geo + reload full config
				_finish_geo_bootstrap
				exit 0
			fi
			bg_wait=$((bg_wait + 1))
		done
		_rollback_failed_start
		runtime_state_record_health "fail" "start:core_not_running"
		echo "  $(date "+%Y-%m-%d %H:%M:%S") - clashoo core not detected within ${_bg_max}s, rolled back" >> /usr/share/clashoo/clashoo.txt
		echo "Clashoo" > "$REAL_LOG"
	) &
}

boot(){
	if [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
		runtime_state_reset
		runtime_state_set "health_status" "starting"
		runtime_state_set "health_detail" "boot_starting"
		runtime_state_set "updated_at" "$(date +%s)"
		# start_service does mihomo -t dry-run + firewall restart + dnsmasq reload/fallback
		# (~20-30s on LXC). Doing this synchronously in boot() blocks rcS S99 stage
		# and during that window sshd/uhttpd refuse connections (kex reset). Detach
		# to background with a small settle delay so the rcS pipeline finishes and
		# management plane stays responsive while clashoo brings the proxy up.
		(
			exec 1000>&- </dev/null >/dev/null 2>&1
			sleep 5
			_clashoo_enabled || exit 0
			/etc/init.d/clashoo start
		) &
	else
		runtime_state_reset
		runtime_state_set "health_status" "stopped"
		runtime_state_set "health_detail" "boot_disabled"
		runtime_state_set "updated_at" "$(date +%s)"
		echo "Clashoo" > "$REAL_LOG"
	fi
	return 0
}

start_service(){
#===========================================================================================================================
	lang=$(uci get luci.main.lang 2>/dev/null)
	enable=$(uci get clashoo.config.enable 2>/dev/null)
	mode=$(uci get clashoo.config.mode 2>/dev/null)
	tun_mode=$(uci get clashoo.config.tun_mode 2>/dev/null)
	core_only=$(uci -q get clashoo.config.core_only 2>/dev/null)

	sync_legacy_core_field
	_migrate_legacy_mihomo_stable
	CLASH="$(_detect_selected_core)"

	cleanup_legacy_startup_backups
	migrate_legacy_clash_dir
	/usr/share/clashoo/migrate_lan_acl.sh >/dev/null 2>&1 || true
	/usr/share/clashoo/migrate_fake_ip_filter.sh >/dev/null 2>&1 || true
	runtime_state_reset

	if [ "${enable:-0}" -eq 1 ]; then
		resolve_effective_tp_modes
		runtime_state_record_preflight "$(selected_core_family)"
		log_msg "Starting Clashoo" "启动 Clashoo"
		if is_singbox_selected; then
			if _core_running; then stop_service >/dev/null 2>&1; fi
			resolve_effective_tp_modes
			runtime_state_record_preflight "singbox"
			if ! _require_runtime_network_stack; then
				runtime_state_record_health "fail" "preflight:missing_fw4_stack"
				echo "Clashoo" > "$REAL_LOG"
				return 1
			fi
			if ! _start_validate_core; then
				runtime_state_record_health "fail" "preflight:core_validation_failed"
				return 1
			fi
			if ! start_singbox_service; then
				# preflight failure already recorded; avoid blanket overwrite
				case "$(grep '^health_detail=' "$RUNTIME_STATE_FILE" 2>/dev/null | cut -d= -f2-)" in
					preflight:*) ;;
					*) runtime_state_record_health "fail" "start:singbox_service_failed" ;;
				esac
				echo "Clashoo" > "$REAL_LOG"
				return 1
			fi
			if [ "$core_only" != "1" ]; then
				log_msg "Configuring firewall rules" "配置防火墙规则"
				backend_apply_rules >/dev/null 2>&1
				singbox_dns_change >/dev/null 2>&1
				add_cron >/dev/null 2>&1
			else
				log_msg "Core-only mode: skipping firewall/DNS takeover" "仅内核模式：跳过防火墙与 DNS 接管"
			fi
			# procd does not launch the named instance until start_service returns
			# and rc.common submits the completed service definition.  Watch it in
			# the background, just like the mihomo path, then roll back only if the
			# registered private instance never becomes healthy.
			(
				exec 1000>&-
				_bg_max="$(_compute_start_wait)"
				_bg_wait=0
				while [ "$_bg_wait" -lt "$_bg_max" ]; do
					sleep 1
					if _singbox_running; then
						runtime_run_health_check_settle >/dev/null 2>&1
						exit 0
					fi
					_bg_wait=$((_bg_wait + 1))
				done
				_rollback_failed_start
				runtime_state_record_health "fail" "start:singbox_not_running"
				echo "  $(date "+%Y-%m-%d %H:%M:%S") - clashoo sing-box not detected within ${_bg_max}s, rolled back" >> /usr/share/clashoo/clashoo.txt
				echo "Clashoo" > "$REAL_LOG"
			) &
			echo "Clashoo" > "$REAL_LOG"
			return 0
		fi
		if _core_running; then stop_service >/dev/null 2>&1; fi
		if ! select_config >/dev/null 2>&1; then
			runtime_state_record_health "fail" "preflight:config_missing"
			_log_runtime_error "Config file is missing or empty; please import a subscription/config first"
			return 1
		fi
	
	if ! _require_runtime_network_stack; then
		runtime_state_record_health "fail" "preflight:missing_fw4_stack"
		echo "Clashoo" > "$REAL_LOG"
		exit 0
	fi
	

if ! _start_validate_core; then
	runtime_state_record_health "fail" "preflight:core_validation_failed"
	return 1
fi

if [ -f "$CONFIG_YAML" ] && [ -s "$CONFIG_YAML" ] ; then

	_dnsmasq_digest_before="$(_dnsmasq_state_digest)"
	_start_prepare_config

	_start_launch_core

else
	if [ "${lang}" = "en" ]; then
		echo "  $(date "+%Y-%m-%d %H:%M:%S") -  problem with config.yaml,  config.yaml is either empty or not found " >> /usr/share/clashoo/clashoo.txt
	elif [ "${lang}" = "zh_cn" ]; then
		echo "  $(date "+%Y-%m-%d %H:%M:%S")  - 你的config.yaml有问题还是为了空 0kb " >> /usr/share/clashoo/clashoo.txt
	fi
	runtime_state_record_health "fail" "start:config_yaml_invalid"
	log_msg "Config file invalid or empty" "配置文件无效或为空"
	echo "Clashoo" > "$REAL_LOG"
fi

else
	runtime_state_set "health_status" "stopped"
	runtime_state_set "health_detail" "service_disabled"
	runtime_state_set "updated_at" "$(date +%s)"
	if [ "${lang}" = "en" ]; then
		echo "  $(date "+%Y-%m-%d %H:%M:%S") - Enable Client and Start Client Again" >> /usr/share/clashoo/clashoo.txt
	elif [ "${lang}" = "zh_cn" ]; then
		echo "  $(date "+%Y-%m-%d %H:%M:%S") - 启用客户端并重新启动客户端" >> /usr/share/clashoo/clashoo.txt
	fi
	echo "Clashoo" > "$REAL_LOG"
fi
#=========================================================================================================================== 
}


remove_mark(){
		/usr/share/clashoo/net/fw4.sh remove >/dev/null 2>&1
		return 0
}

stop_service(){
#===========================================================================================================================
		# The updater restarts once after an in-place APK upgrade. Skip the
		# package-manager pre-deinstall stop to avoid its ~120s script wait.
		if [ -r /var/run/clashoo_package_upgrade ]; then
			_upgrade_pid="$(cat /var/run/clashoo_package_upgrade 2>/dev/null)"
			case "$_upgrade_pid" in
				''|*[!0-9]*) rm -f /var/run/clashoo_package_upgrade ;;
				*)
					[ -d "/proc/$_upgrade_pid" ] && return 0
					rm -f /var/run/clashoo_package_upgrade
					;;
			esac
		fi
		
			lang=$(uci get luci.main.lang 2>/dev/null)
			tun_mode=$(uci get clashoo.config.tun_mode 2>/dev/null)

			# server.list is read-only; just clean up merged temp files in /tmp
			rm -f /tmp/clashoo/fake_filter.list /tmp/clashoo/fake_filter.yaml 2>/dev/null

			log_msg "Stopping Clashoo" "正在停止 Clashoo"
			for _ac_sig in TERM KILL; do
				for _ac_pid in $(pgrep -f access_check_daemon 2>/dev/null); do
					tr '\0' ' ' < "/proc/$_ac_pid/cmdline" 2>/dev/null | grep -q access_check_daemon.sh && kill -"$_ac_sig" "$_ac_pid" 2>/dev/null
				done
			done
			rm -rf /tmp/clashoo/access_check_daemon.pid /tmp/clashoo/access_check_daemon.lock 2>/dev/null
			rm -f /var/run/clashoo_bootstrap_pending 2>/dev/null
		log_msg "Removing firewall rules" "清理防火墙规则"
		backend_remove_rules >/dev/null 2>&1
		cleanup_stale_core_nft >/dev/null 2>&1
		revert_dns >/dev/null 2>&1

			sh /usr/share/clashoo/runtime/backup.sh >/dev/null 2>&1

			stop_singbox_service
			_kill_core

		rm -rf /www/luci-static/yacd 2>/dev/null

  	  	del_cron >/dev/null 2>&1


	 	echo "" >/usr/share/clashoo/clashoo.txt >/dev/null 2>&1

		echo "0" > /usr/share/clashoo/logstatus_check >/dev/null 2>&1

		if [ "${lang}" = "en" ]; then
			echo "  $(date "+%Y-%m-%d %H:%M:%S") - CLIENT IS DISABLED " >> /usr/share/clashoo/clashoo.txt
		elif [ "${lang}" = "zh_cn" ]; then
			echo "  $(date "+%Y-%m-%d %H:%M:%S")  - 客户端被禁用 " >> /usr/share/clashoo/clashoo.txt
		fi
		echo "Clashoo" > "$REAL_LOG"
	
		if _core_running; then
			_kill_core
		fi
		for _cpid in $(pgrep -f /etc/clashoo 2>/dev/null); do
			_ccl="$(tr '\0' ' ' < "/proc/$_cpid/cmdline" 2>/dev/null)"
			printf '%s' "$_ccl" | grep -q -e '-d /etc/clashoo' -e '-f /etc/clashoo/config.yaml' && kill "$_cpid" 2>/dev/null
		done
		runtime_state_reset
		runtime_state_set "health_status" "stopped"
		runtime_state_set "health_detail" "service_stopped"
		runtime_state_set "updated_at" "$(date +%s)"

		# kick access check on stop (proxy_listening fast-paths to down; UI reflects stop within 5s)
		(exec 1000>&-; /usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1) &

#===========================================================================================================================
}

_mihomo_reload_api() {
	local _path="$1" _response="$2" _dp _ds _lip
	_dp="$(uci -q get clashoo.config.dash_port)"; [ -n "$_dp" ] || _dp=9090
	_ds="$(uci -q get clashoo.config.dash_pass)"
	_lip="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}')"
	[ -n "$_lip" ] || _lip=127.0.0.1
	MIHOMO_RELOAD_CURL_RC=0
	MIHOMO_RELOAD_HTTP_CODE="$(curl -sS -m 5 -o "$_response" -w '%{http_code}' \
		-H "Authorization: Bearer ${_ds}" -H "Content-Type: application/json" \
		-X PUT -d '{"path":"'"$_path"'"}' \
		"http://${_lip}:${_dp}/configs?force=true" 2>/dev/null)" || MIHOMO_RELOAD_CURL_RC=$?
	[ "$MIHOMO_RELOAD_CURL_RC" -eq 0 ] || return 1
	case "$MIHOMO_RELOAD_HTTP_CODE" in
		2??) return 0 ;;
		*) return 1 ;;
	esac
}

_mihomo_reload_save_dns() {
	MIHOMO_DHCP_MODE="$(stat -c %a /etc/config/dhcp)" || return 1
	cp /etc/config/dhcp "$1"
}

_mihomo_reload_restore_dns() {
	cp "$1" /etc/config/dhcp && chmod "$MIHOMO_DHCP_MODE" /etc/config/dhcp &&
		uci -q revert dhcp && _reload_dnsmasq >/dev/null 2>&1
}

_mihomo_reload_restore() {
	local _previous="$1" _response="$2" _dns_backup="$3" _peripheral_changed="$4" _restore _failed=0
	if ! _mihomo_reload_api "$_previous" "$_response"; then
		return 1
	fi
	if [ "$_peripheral_changed" = "1" ]; then
		if _restore="$(mktemp "${CLASH_CONFIG}/.reload-restore.XXXXXX")"; then
			cp "$_previous" "$_restore" && mv -f "$_restore" "$CONFIG_YAML" || { rm -f "$_restore"; _failed=1; }
		else
			_failed=1
		fi
		_mihomo_reload_restore_dns "$_dns_backup" || _failed=1
		backend_remove_rules >/dev/null 2>&1 || _failed=1
		backend_apply_rules >/dev/null 2>&1 || _failed=1
	fi
	return "$_failed"
}

reload_service()
{
	if is_singbox_selected; then
		if _singbox_running; then
			# Use rc.common's transaction wrapper so the complete Clashoo service
			# definition is rebuilt before the named instance is registered again.
			# This intentionally reapplies Clashoo's own firewall/DNS state; calling
			# start_service directly would skip that wrapper and procd setup.
			stop_singbox_service >/dev/null 2>&1
			start
		fi
	elif _core_running; then
		local _candidate _previous _response _dns_backup _source _pf_rc _error _peripheral_changed
		_error=""
		resolve_effective_tp_modes
		_candidate="$(mktemp "${CLASH_CONFIG}/.reload-candidate.XXXXXX")" || { _log_runtime_error "Mihomo reload failed: cannot stage candidate"; return 1; }
		_previous="$(mktemp "${CLASH_CONFIG}/.reload-previous.XXXXXX")" || { rm -f "$_candidate"; _log_runtime_error "Mihomo reload failed: cannot back up current config"; return 1; }
		_response="$(mktemp /tmp/clashoo-reload-response.XXXXXX)" || { rm -f "$_candidate" "$_previous"; _log_runtime_error "Mihomo reload failed: cannot stage API response"; return 1; }
		_dns_backup="$(mktemp "${CLASH_CONFIG}/.reload-dhcp.XXXXXX")" || { rm -f "$_candidate" "$_previous" "$_response"; _log_runtime_error "Mihomo reload failed: cannot back up DNS state"; return 1; }
		_source="$(uci -q get clashoo.config.use_config 2>/dev/null)"
		if [ ! -s "$_source" ]; then
			_error="selected config is missing or empty"
		elif ! cp "$_source" "$_candidate" || ! cp "$CONFIG_YAML" "$_previous" ||
			! _mihomo_reload_save_dns "$_dns_backup"; then
			_error="cannot back up current config or DNS state"
		elif ! ( CONFIG_YAML="$_candidate"; CLASHOO_RELOAD_CANDIDATE=1; _start_prepare_config ) >/dev/null 2>&1 ||
			[ ! -s "$_candidate" ]; then
			_error="candidate generation failed; check DNS and core settings"
		fi
		rm -f "${_candidate}.mixin" 2>/dev/null
		if [ -n "$_error" ]; then
			_log_runtime_error "Mihomo reload failed: ${_error}; current config preserved"
			rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
			return 1
		fi
		if ! _mihomo_proxy_dns_valid "$_candidate"; then
			_log_runtime_error "Mihomo reload failed: node DNS policy or respect-rules requires a Proxy Node Domain Resolver; current config preserved"
			rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
			return 1
		fi
		if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ]; then
			SAFE_PATHS=/ _run_with_timeout 15 "$CLASH" -d "$CLASH_CONFIG" -f "$_candidate" -t >> "$CORE_LOG_PATH" 2>&1
		else
			_run_with_timeout 15 "$CLASH" -d "$CLASH_CONFIG" -f "$_candidate" -t >> "$CORE_LOG_PATH" 2>&1
		fi
		_pf_rc=$?
		if [ "$_pf_rc" -ne 0 ]; then
			if [ "$_pf_rc" -eq 124 ]; then
				_error="Mihomo reload failed: candidate preflight timed out"
			else
				_error="Mihomo reload failed: candidate config invalid (exit ${_pf_rc}; see core log)"
			fi
			_log_runtime_error "$_error; current config preserved"
			rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
			return 1
		fi
		if ! _mihomo_reload_api "$_candidate" "$_response"; then
			_error="Mihomo reload failed (curl ${MIHOMO_RELOAD_CURL_RC}, HTTP ${MIHOMO_RELOAD_HTTP_CODE})"
			if _mihomo_reload_restore "$_previous" "$_response" "$_dns_backup" 0; then
				_log_runtime_error "${_error}; previous config restored"
				rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
			else
				_log_runtime_error "${_error}; reload outcome unknown; candidate and backup retained"
				rm -f "$_response" "$_dns_backup"
			fi
			return 1
		fi
		_peripheral_changed=0
		if ! mv -f "$_candidate" "$CONFIG_YAML"; then
			_error="Mihomo reload failed: cannot commit candidate config"
		elif [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then
			_peripheral_changed=1
			if ! backend_remove_rules >/dev/null 2>&1 || ! backend_apply_rules >/dev/null 2>&1 ||
				! yml_dns_change >/dev/null 2>&1 || ! _reload_dnsmasq >/dev/null 2>&1; then
				_error="Mihomo reload failed: firewall or DNS update failed"
			fi
		fi
		if [ -n "$_error" ]; then
			if _mihomo_reload_restore "$_previous" "$_response" "$_dns_backup" "$_peripheral_changed"; then
				_log_runtime_error "${_error}; previous config and settings restored"
				rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
			else
				_log_runtime_error "${_error}; rollback incomplete; previous config retained for recovery"
				rm -f "$_candidate" "$_response"
			fi
			return 1
		fi
		rm -f "$_previous" "$_response" "$_dns_backup" "${CONFIG_YAML}.dns-leak-nameserver"
		runtime_state_record_preflight "mihomo"
		runtime_run_health_check_settle >/dev/null 2>&1
	fi
}

status_service() {
	if _selected_core_running; then
		echo "running"
	else
		echo "inactive"
	fi
}
