#!/bin/sh
# OpenClash is the sole owner of the core, firewall, routes and DNS.
# The adapter accepts only fixed actions, and imports YAML via stdin.
# OpenWrt's native shell libraries intentionally read optional unset variables.
# Do not impose nounset on them (e.g. IPKG_INSTROOT on a running router).
set -e
umask 077
. /usr/share/libubox/jshn.sh
. /lib/functions.sh

PROFILE=/etc/openclash/config/xxyun.yaml
MARKER=/etc/xxyun/openclash-profile-owned
INIT=/etc/init.d/openclash
STAGED=
MARKER_STAGED=

cleanup() {
  [ -z "$STAGED" ] || rm -f "$STAGED"
  [ -z "$MARKER_STAGED" ] || rm -f "$MARKER_STAGED"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

fail() {
  json_init
  json_add_boolean ok 0
  json_add_object error
  json_add_string code "$1"
  json_add_string message "$2"
  if [ "${3-0}" = 1 ]; then json_add_boolean profileCommitted 1; fi
  json_close_object
  json_dump
  exit 1
}

get_config() { uci -q get "openclash.config.$1" 2>/dev/null || :; }

# Native UCI selection can change before the running core is replaced. Authorize
# implicit restart/stop against the actual fixed -f argument, not its process
# name alone. This is an ownership check, never a connectivity prerequisite.
read_native_process_owner() {
  local pids
  NATIVE_PROCESS_OWNER=none
  pids=$(pidof clash 2>/dev/null || :)
  [ -n "$pids" ] || return 0
  NATIVE_PROCESS_OWNER=unknown
  set -- $pids
  [ "$#" = 1 ] || return 0
  case "$1" in ''|*[!0-9]*) return 0;; esac
  NATIVE_PROCESS_OWNER=$(ruby -e '
begin
  raw = File.open("/proc/#{ARGV[0]}/cmdline", "rb") { |f| f.read(65537) }
  abort if !raw || raw.bytesize > 65536 || !raw.end_with?("\0")
  args = raw.split("\0", -1)
  indexes = args.each_index.select { |i| args[i] == "-f" }
  abort unless indexes.length == 1
  path = args[indexes[0] + 1]
  abort if !path || path.empty?
  puts(path == "/etc/openclash/xxyun.yaml" ? "owned" : "other")
rescue StandardError
  exit 1
end
' "$1" 2>/dev/null) || NATIVE_PROCESS_OWNER=unknown
}

check_subscription() {
  local section="$1" name enabled
  config_get name "$section" name
  config_get_bool enabled "$section" enabled 1
  if [ "$name" = xxyun ] && [ "$enabled" = 1 ]; then
    AUTO_DISABLED=0
  fi
}

disable_owned_subscription() {
  local section="$1" name enabled
  config_get name "$section" name
  config_get_bool enabled "$section" enabled 1
  if [ "$name" = xxyun ] && [ "$enabled" = 1 ]; then
    uci -q set "openclash.$section.enabled=0" || return 1
    SUB_CHANGED=1
  fi
}

ensure_single_subscription_owner() {
  SUB_CHANGED=0
  config_load openclash
  config_foreach disable_owned_subscription config_subscribe
  if [ "$SUB_CHANGED" = 1 ]; then uci -q commit openclash; fi
}

read_status() {
  INSTALLED=0 CORE_PRESENT=0 PROFILE_EXISTS=0 PROFILE_OWNED=0 ACTIVE=0 RUNNING=0 ENABLED=0
  AUTO_DISABLED=1 OTHER_ACTIVE=0
  [ ! -x "$INIT" ] || INSTALLED=1
  CORE=/etc/openclash/clash
  if [ ! -x "$CORE" ]; then
    if [ "$(get_config small_flash_memory)" = 1 ]; then
      CORE=/tmp/etc/openclash/core/clash_meta
    else
      CORE=/etc/openclash/core/clash_meta
    fi
  fi
  [ ! -x "$CORE" ] || CORE_PRESENT=1
  [ ! -f "$PROFILE" ] || PROFILE_EXISTS=1
  if [ -f "$MARKER" ] && [ ! -L "$MARKER" ] && [ "$(cat "$MARKER")" = xxyun-openclash-profile-v1 ]; then
    PROFILE_OWNED=1
  fi
  SELECTED=$(get_config config_path)
  if [ "$SELECTED" = "$PROFILE" ] && [ "$PROFILE_OWNED" = 1 ]; then ACTIVE=1; fi
  [ "$(get_config enable)" != 1 ] || ENABLED=1
  # This matches OpenClash's own native status; it is not a connectivity probe.
  if pidof clash >/dev/null 2>&1; then RUNNING=1; fi
  if [ -n "$SELECTED" ] && [ "$ACTIVE" = 0 ]; then OTHER_ACTIVE=1; fi
  if [ "$INSTALLED" = 1 ]; then
    config_load openclash
    config_foreach check_subscription config_subscribe
  fi
}

emit_status() {
  read_status
  json_init
  json_add_boolean ok 1
  json_add_object data
  json_add_boolean installed "$INSTALLED"
  json_add_boolean corePresent "$CORE_PRESENT"
  json_add_boolean profileExists "$PROFILE_EXISTS"
  json_add_boolean profileOwned "$PROFILE_OWNED"
  json_add_boolean active "$ACTIVE"
  json_add_boolean running "$RUNNING"
  json_add_boolean enabled "$ENABLED"
  json_add_boolean autoUpdateDisabled "$AUTO_DISABLED"
  json_add_boolean otherProfileActive "$OTHER_ACTIVE"
  json_close_object
  json_dump
}

[ "$#" = 1 ] || fail INVALID_ARGUMENT '只接受一个固定操作名称。'
case "$1" in status|import|import_prepared|apply|start|stop|remove) ;; *) fail INVALID_ARGUMENT '不支持此操作。';; esac
[ "$(id -u)" = 0 ] || fail PERMISSION_DENIED '需要路由器管理权限。'
if [ "$1" = status ]; then emit_status; exit 0; fi
mkdir -p /var/lock
exec 9>/var/lock/xxyun-openclash.lock
flock -n 9 || fail BUSY '另一项配置操作正在进行，请稍后重试。'
read_status
# A reserved basename alone does not prove ownership of a pre-existing config.
if { [ "$1" = stop ] || [ "$1" = remove ]; } && [ "$PROFILE_OWNED" = 0 ]; then
  emit_status
  exit 0
fi
if [ "$INSTALLED" != 1 ]; then
  # First login/logout must work before OpenClash has been installed.
  if { [ "$1" = remove ] || [ "$1" = stop ]; } && [ "$PROFILE_EXISTS" = 0 ] && [ "$ACTIVE" = 0 ]; then
    if [ "$1" = remove ]; then rm -f "$MARKER"; fi
    emit_status
    exit 0
  fi
  fail OPENCLASH_NOT_INSTALLED '请先安装代理服务。'
fi

case "$1" in
  import|import_prepared|apply)
    [ "$CORE_PRESENT" = 1 ] || fail CORE_NOT_INSTALLED '请先安装代理内核。'
    # A one-time installation completion may populate this account's cache, but
    # never applies over a running or separately selected native instance.
    if [ "$1" = import_prepared ]; then
      [ "$RUNNING" = 0 ] && [ "$OTHER_ACTIVE" = 0 ] || fail PREPARE_DEFERRED '安装组件已就绪，现有连接和配置已保留。'
    fi
    if { [ -e "$PROFILE" ] || [ -L "$PROFILE" ]; } && [ "$PROFILE_OWNED" = 0 ]; then
      fail PROFILE_CONFLICT '代理服务已有同名的外部 xxyun.yaml 配置。请先在原生配置管理中改名，原配置未被修改。'
    fi
    [ ! -L "$PROFILE" ] || fail INVALID_PROFILE 'xxyun 配置不能是符号链接。'
    mkdir -p /etc/openclash/config
    STAGED=$(mktemp /etc/openclash/config/.xxyun-import.XXXXXX)
    # Match the native upload size limit. Read at most limit+1, never an unbounded file.
    head -c 10485761 >"$STAGED"
    SIZE=$(wc -c <"$STAGED")
    [ "$SIZE" -gt 0 ] && [ "$SIZE" -le 10485760 ] || fail INVALID_CONFIG_SIZE '节点配置为空或超过 10 MiB。'
    if ! timeout 45 "$CORE" -t -d /etc/openclash -f "$STAGED" >/dev/null 2>&1; then
      fail NATIVE_VALIDATION_FAILED '代理服务内核未通过配置校验，原配置已保留。'
    fi
    if [ "$1" = apply ]; then
      read_status
      [ "$OTHER_ACTIVE" = 0 ] || fail NATIVE_OWNER_MISMATCH '当前选中其他配置，未应用链式设置。'
      if [ "$RUNNING" = 1 ]; then
        read_native_process_owner
        [ "$ACTIVE" = 1 ] && [ "$NATIVE_PROCESS_OWNER" = owned ] || fail NATIVE_OWNER_MISMATCH '当前运行进程不属于此配置，未应用链式设置。'
      fi
    fi
    if [ "$1" = import_prepared ]; then
      read_status
      [ "$RUNNING" = 0 ] && [ "$OTHER_ACTIVE" = 0 ] || fail PREPARE_DEFERRED '安装组件已就绪，现有连接和配置已保留。'
    fi
    if [ "$PROFILE_OWNED" = 0 ]; then
      mkdir -p /etc/xxyun
      chmod 700 /etc/xxyun
      MARKER_STAGED=$(mktemp /etc/xxyun/.openclash-owned.XXXXXX)
      printf '%s\n' xxyun-openclash-profile-v1 >"$MARKER_STAGED"
      chmod 600 "$MARKER_STAGED"
      mv -f "$MARKER_STAGED" "$MARKER"
      MARKER_STAGED=
    fi
    ensure_single_subscription_owner
    # A user-requested apply must retry native application even if a previous
    # import saved these exact bytes but its native restart did not complete.
    # Ordinary subscription imports keep their no-op behavior for equal files.
    if { [ "$1" = import ] || [ "$1" = import_prepared ]; } && [ -f "$PROFILE" ] && cmp -s "$STAGED" "$PROFILE"; then
      rm -f "$STAGED"
      STAGED=
      emit_status
      exit 0
    fi
    chmod 600 "$STAGED"
    mv -f "$STAGED" "$PROFILE"
    STAGED=
    # Import never activates a different profile or starts a stopped service.
    read_status
    if [ "$1" != import_prepared ] && [ "$ACTIVE" = 1 ] && [ "$RUNNING" = 1 ]; then
      read_native_process_owner
      if [ "$NATIVE_PROCESS_OWNER" = owned ]; then
        # Keep the lock in this helper, never in native background descendants.
        "$INIT" restart 9>&- >/dev/null 2>&1 || fail NATIVE_RESTART_FAILED '配置已保存，代理服务重启失败；请查看代理状态。' 1
      elif [ "$1" = apply ]; then
        fail NATIVE_RESTART_FAILED '配置已保存，但运行配置归属已变化，未重启其他代理。' 1
      fi
    fi
    ;;
  start)
    [ "$PROFILE_OWNED" = 1 ] || fail PROFILE_CONFLICT '此同名配置不属于 xxyun，请先更新节点或处理原配置。'
    [ "$PROFILE_EXISTS" = 1 ] || fail CONFIG_NOT_FOUND '请先更新节点。'
    [ "$CORE_PRESENT" = 1 ] || fail CORE_NOT_INSTALLED '请先安装代理内核。'
    ensure_single_subscription_owner
    # An explicit Activate click is the only operation that changes profile ownership.
    uci -q set "openclash.config.config_path=$PROFILE"
    uci -q set openclash.config.enable=1
    uci -q commit openclash
    "$INIT" restart 9>&- >/dev/null 2>&1 || fail NATIVE_START_FAILED '代理服务启动失败；请查看代理状态。'
    ;;
  stop|remove)
    # Do not modify native selection, cleanup references or credentials while
    # an unrelated/ambiguous core still runs under this UCI selection.
    read_native_process_owner
    if [ "$(get_config config_path)" = "$PROFILE" ]; then
      case "$NATIVE_PROCESS_OWNER" in
        owned|none) ;;
        *) fail NATIVE_OWNER_MISMATCH '当前运行进程无法归属此配置，未停止或清理其他代理；请确认原生配置切换已完成。';;
      esac
    else
      # The inverse transition is also possible: another profile is selected
      # while the previous account's core is still running. Do not delete its
      # credentials or claim stop completed before native switching finishes.
      case "$NATIVE_PROCESS_OWNER" in
        owned|unknown) fail NATIVE_OWNER_MISMATCH '原生配置切换尚未完成，未停止或清理配置；请稍后重试。';;
      esac
    fi
    if [ "$1" = remove ]; then ensure_single_subscription_owner; fi
    OWNED_STOPPED=0
    # Never stop another selected profile, even if the same native process name exists.
    if [ "$(get_config config_path)" = "$PROFILE" ]; then
      uci -q set openclash.config.enable=0
      uci -q commit openclash
      "$INIT" stop 9>&- >/dev/null 2>&1 || fail NATIVE_STOP_FAILED '代理服务停止失败；请查看代理状态。'
      OWNED_STOPPED=1
      if [ "$1" = remove ] && [ "$(get_config config_path)" = "$PROFILE" ]; then
        uci -q delete openclash.config.config_path
        uci -q commit openclash
      fi
    fi
    if [ "$1" = remove ]; then
      # Use the same ownership-aware UCI cleanup as native Config Manage.
      # The filename and Lua program are fixed; no browser / subscription input is code.
      lua -e 'require("luci.openclash").config_refs("xxyun.yaml")' >/dev/null 2>&1 ||
        fail NATIVE_CLEANUP_FAILED '代理服务配置引用未能清理，请稍后重试。'
      CACHE_TARGET=$(readlink /etc/openclash/cache.db 2>/dev/null || :)
      if [ "$CACHE_TARGET" = /etc/openclash/history/xxyun.db ]; then
        rm -f /etc/openclash/cache.db
      elif [ "$OWNED_STOPPED" = 1 ] && [ "$CACHE_TARGET" = /tmp/etc/openclash/cache.db ]; then
        rm -f /etc/openclash/cache.db /tmp/etc/openclash/cache.db
      fi
      rm -f "$PROFILE" /etc/openclash/xxyun.yaml /tmp/xxyun.yaml \
        /etc/openclash/history/xxyun.db /etc/openclash/history/xxyun
      rm -f "$MARKER"
    fi
    ;;
esac
emit_status
