#!/usr/bin/env bash
set -Eeuo pipefail

CLASH_HOME="/root/clash"
CONFIG_SRC="${CLASH_HOME}/clash.yaml"
CONFIG_DST="${CLASH_CONFIG_PATH:-/root/.config/clash/config.yaml}"
CLASH_DIR="/root/.config/clash"
MIHOMO_DIR="/root/.config/mihomo"
DOWNLOAD_DIR="${CLASH_HOME}/downloads"
BASE_URL="${CLASH_BASE_URL:-https://v5lzzuocozpnci5.wsr888.top/Y6cLxn8hPUB1Z78/folder/clash}"
CONFIG_URL="${CONFIG_URL:-${BASE_URL}/clash.yaml}"
MMDB_URL="${MMDB_URL:-${BASE_URL}/Country.mmdb}"
GEOIP_METADB_URL="${GEOIP_METADB_URL:-https://v5lzzuocozpnci5.wsr888.top/Y6cLxn8hPUB1Z78/folder/clash/geoip.metadb}"
GEOX_GEOIP_URL="${GEOX_GEOIP_URL:-${GEOIP_METADB_URL}}"
GEOSITE_URL="${GEOSITE_URL:-${BASE_URL}/geosite.dat}"
ASN_URL="${ASN_URL:-${BASE_URL}/GeoLite2-ASN.mmdb}"
MIHOMO_URL="${MIHOMO_URL:-${BASE_URL}/mihomo-linux-amd64-compatible-v1.19.24.gz}"
MIHOMO_GZ="${DOWNLOAD_DIR}/mihomo-linux-amd64-compatible-v1.19.24.gz"
MIHOMO_BIN="${DOWNLOAD_DIR}/mihomo-linux-amd64-compatible-v1.19.24"
GEOIP_METADB_PATH="${MIHOMO_DIR}/geoip.metadb"
SERVICE_FILE="/etc/systemd/system/clash.service"
DOCKER_PROXY_FILE="/etc/systemd/system/docker.service.d/http-proxy.conf"
API_URL="${CLASH_API_URL:-http://127.0.0.1:9090}"
HTTP_PROXY_ADDR="${HTTP_PROXY_ADDR:-http://127.0.0.1:7890}"
SOCKS_PROXY_ADDR="${SOCKS_PROXY_ADDR:-socks5://127.0.0.1:7891}"
DEFAULT_PROXY_FILE="${CLASH_HOME}/.default_proxy"
CLASH_RUN_DIR="${MIHOMO_DIR}/run"
CLASH_RUNNER="${CLASH_HOME}/run-clash.sh"
CLASH_PID_FILE="${CLASH_RUN_DIR}/clash.pid"
CLASH_LOG_FILE="${CLASH_RUN_DIR}/clash.log"
SUPERVISOR_PROGRAM="${CLASH_SUPERVISOR_PROGRAM:-clash}"
SUPERVISOR_CONF_DIR="${CLASH_SUPERVISOR_CONF_DIR:-/etc/supervisor/conf.d}"
SUPERVISOR_CONF_FILE="${SUPERVISOR_CONF_DIR}/${SUPERVISOR_PROGRAM}.conf"
S6_SERVICE_DIR="${CLASH_S6_SERVICE_DIR:-/etc/services.d/clash}"
RUNIT_SERVICE_DIR="${CLASH_RUNIT_SERVICE_DIR:-/etc/sv/clash}"
RUNIT_SERVICE_LINK_DIR="${CLASH_RUNIT_SERVICE_LINK_DIR:-/etc/service}"
OPENRC_SERVICE_FILE="${CLASH_OPENRC_SERVICE_FILE:-/etc/init.d/clash}"
NON_INTERACTIVE=0
FORCE=0
AUTO_SELECT=1
PREFER_REGIONS="HK,TW,JP"
TARGET_PROXY_NAME=""
ROTATE_NEXT=0
CLASH_APT_MARKER_DIR="${CLASH_APT_MARKER_DIR:-}"
SELECTED_COLOR=""
COLOR_RESET=""
SKIP_DOCKER_PROXY=0

log() {
  printf '[%s] [clash] %s\n' "$(date '+%F %T')" "$*"
}

init_colors() {
  if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
    SELECTED_COLOR=$'\033[31m'
    COLOR_RESET=$'\033[0m'
  else
    SELECTED_COLOR=""
    COLOR_RESET=""
  fi
}

highlight_selected() {
  local text="$1"
  printf '%s%s%s' "${SELECTED_COLOR}" "${text}" "${COLOR_RESET}"
}

mark_apt_state() {
  local state="$1"
  if [[ -z "${CLASH_APT_MARKER_DIR}" ]]; then
    return 0
  fi
  mkdir -p "${CLASH_APT_MARKER_DIR}"
  printf '%s\n' "${state}" > "${CLASH_APT_MARKER_DIR}/apt.state"
}

finalize_apt_state_success() {
  local current_state=""
  if [[ -n "${CLASH_APT_MARKER_DIR}" && -f "${CLASH_APT_MARKER_DIR}/apt.state" ]]; then
    current_state="$(<"${CLASH_APT_MARKER_DIR}/apt.state")"
  fi
  case "${current_state}" in
    running)
      mark_apt_state "done"
      ;;
    pending|"")
      mark_apt_state "skipped"
      ;;
  esac
}

usage() {
  cat <<'EOF'
Usage:
  deploy_clash_oneclick.sh [--non-interactive] [--force] [--prefer-region HK,TW,JP|NONE] [--skip-docker-proxy] [--rotate-next] [proxy-name]

Options:
  --non-interactive      Never prompt. Existing installs are overwritten.
  --force                Overwrite existing clash files/config.
  --prefer-region VALUE  Prefer fastest node from listed regions before global fastest.
                         Default: HK,TW,JP. Use comma-separated values or NONE to disable.
  --no-auto-select       Deploy only, do not auto select a proxy.
  --skip-docker-proxy    Do not configure docker proxy in this script.
  --rotate-next          Switch PROXY group to the next selectable node.
  proxy-name             If clash is already installed, switch PROXY group to this node directly.
EOF
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --non-interactive)
        NON_INTERACTIVE=1
        FORCE=1
        shift
        ;;
      --force)
        FORCE=1
        shift
        ;;
      --prefer-region)
        PREFER_REGIONS="${2:-HK,TW,JP}"
        shift 2
        ;;
      --no-auto-select)
        AUTO_SELECT=0
        shift
        ;;
      --skip-docker-proxy)
        SKIP_DOCKER_PROXY=1
        shift
        ;;
      --rotate-next)
        ROTATE_NEXT=1
        shift
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        TARGET_PROXY_NAME="$*"
        break
        ;;
    esac
  done
}

retry() {
  local max_attempts="$1"
  local sleep_seconds="$2"
  shift 2
  local attempt=1
  while true; do
    if "$@"; then
      return 0
    fi
    if (( attempt >= max_attempts )); then
      log "command failed after ${attempt} attempts: $*"
      return 1
    fi
    log "retry ${attempt}/${max_attempts} failed: $*"
    sleep "$sleep_seconds"
    attempt=$((attempt + 1))
  done
}

ensure_dirs() {
  mkdir -p "${CLASH_DIR}" "${CLASH_DIR}/ruleset" "${MIHOMO_DIR}" "${DOWNLOAD_DIR}" "${CLASH_RUN_DIR}"
}

apt_install_yaml() {
  if command -v apt-get >/dev/null 2>&1; then
    DEBIAN_FRONTEND=noninteractive apt-get \
      -o DPkg::Lock::Timeout=600 \
      -o Dpkg::Options::=--force-confdef \
      -o Dpkg::Options::=--force-confold \
      install -y python3-yaml >/dev/null 2>&1 && return 0
  fi
  return 1
}

ensure_python_yaml() {
  if python3 - <<'PY' >/dev/null 2>&1
import yaml
PY
  then
    mark_apt_state "skipped"
    return 0
  fi
  mark_apt_state "running"
  if retry 3 3 apt_install_yaml; then
    mark_apt_state "done"
    return 0
  fi
  mark_apt_state "done"
  if command -v pip3 >/dev/null 2>&1; then
    retry 3 3 pip3 install --break-system-packages --quiet pyyaml
    return 0
  fi
  if command -v pip >/dev/null 2>&1; then
    retry 3 3 pip install --break-system-packages --quiet pyyaml
    return 0
  fi
  if retry 3 3 python3 -m ensurepip --upgrade >/dev/null 2>&1; then
    retry 3 3 python3 -m pip install --break-system-packages --quiet pyyaml
    return 0
  fi
  return 1
}

require_config() {
  retry 5 3 curl -L --fail --output "${CONFIG_SRC}" "${CONFIG_URL}"
}

has_clash() {
  command -v clash >/dev/null 2>&1 || [[ -x /usr/local/bin/clash ]]
}

pid1_name() {
  cat /proc/1/comm 2>/dev/null || echo unknown
}

systemd_pkg_present() {
  command -v systemctl >/dev/null 2>&1 && ([[ -x /lib/systemd/systemd ]] || [[ -x /usr/lib/systemd/systemd ]])
}

has_systemd() {
  [[ -d /run/systemd/system ]] || return 1
  [[ "$(pid1_name)" == "systemd" ]]
}

pid1_is_supervisord() {
  [[ "$(pid1_name)" == "supervisord" ]]
}

pid1_is_s6() {
  [[ "$(pid1_name)" == "s6-svscan" ]]
}

pid1_is_runit() {
  [[ "$(pid1_name)" == "runit" ]]
}

pid1_is_openrc() {
  local name
  name="$(pid1_name)"
  [[ "${name}" == "openrc-init" || "${name}" == "init" ]]
}

has_pid1_supervisor() {
  pid1_is_supervisord || return 1
  command -v supervisorctl >/dev/null 2>&1 || return 1
  supervisorctl status >/dev/null 2>&1
}

has_pid1_s6() {
  pid1_is_s6 || return 1
  command -v s6-svc >/dev/null 2>&1 || return 1
}

has_pid1_runit() {
  pid1_is_runit || return 1
  command -v sv >/dev/null 2>&1 || return 1
}

has_pid1_openrc() {
  pid1_is_openrc || return 1
  command -v rc-service >/dev/null 2>&1 || return 1
  command -v rc-update >/dev/null 2>&1 || return 1
  [[ -d /run/openrc ]] || [[ -f /run/openrc/softlevel ]] || [[ -f /lib/rc/sh/init.sh ]]
}

runtime_manager() {
  if has_systemd; then
    printf 'systemd\n'
    return 0
  fi
  if has_pid1_supervisor; then
    printf 'pid1-supervisor\n'
    return 0
  fi
  if has_pid1_s6; then
    printf 'pid1-s6\n'
    return 0
  fi
  if has_pid1_runit; then
    printf 'pid1-runit\n'
    return 0
  fi
  if has_pid1_openrc; then
    printf 'pid1-openrc\n'
    return 0
  fi
  printf 'direct\n'
}

runtime_manager_explain() {
  local manager init_name
  manager="$(runtime_manager)"
  init_name="$(pid1_name)"
  case "${manager}" in
    systemd)
      printf 'PID 1 is systemd and /run/systemd/system is present\n'
      ;;
    pid1-supervisor)
      printf 'PID 1 is supervisord and supervisorctl is reachable\n'
      ;;
    pid1-s6)
      printf 'PID 1 is s6-svscan and s6-svc is available\n'
      ;;
    pid1-runit)
      printf 'PID 1 is runit and sv is available\n'
      ;;
    pid1-openrc)
      printf 'PID 1 is %s and OpenRC tools are available\n' "${init_name}"
      ;;
    *)
      if systemd_pkg_present; then
        printf 'PID 1 is %s; systemd packages are installed but not active in this boot\n' "${init_name}"
      else
        printf 'PID 1 is %s; no supported native service manager is active\n' "${init_name}"
      fi
      ;;
  esac
}

bool_word() {
  if "$@"; then
    printf 'yes'
  else
    printf 'no'
  fi
}

log_runtime_manager_diagnostics() {
  local stage manager init_name
  stage="${1:-current}"
  manager="$(runtime_manager)"
  init_name="$(pid1_name)"
  log "runtime diagnostics (${stage}): pid1=${init_name} manager=${manager}"
  log "runtime diagnostics (${stage}): $(runtime_manager_explain)"
  log "runtime diagnostics (${stage}): systemd_active=$(bool_word has_systemd) systemd_pkg=$(bool_word systemd_pkg_present)"
  log "runtime diagnostics (${stage}): supervisor=$(bool_word has_pid1_supervisor) s6=$(bool_word has_pid1_s6) runit=$(bool_word has_pid1_runit) openrc=$(bool_word has_pid1_openrc)"
}

has_docker() {
  command -v docker >/dev/null 2>&1 || (has_systemd && systemctl list-unit-files 2>/dev/null | grep -q '^docker\.service')
}

apt_update_once() {
  if command -v apt-get >/dev/null 2>&1; then
    DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 update >/dev/null 2>&1
    return 0
  fi
  return 1
}

apt_install_systemd() {
  if command -v apt-get >/dev/null 2>&1; then
    DEBIAN_FRONTEND=noninteractive apt-get \
      -o DPkg::Lock::Timeout=600 \
      -o Dpkg::Options::=--force-confdef \
      -o Dpkg::Options::=--force-confold \
      install -y systemd systemd-sysv dbus >/dev/null 2>&1 && return 0
  fi
  return 1
}

ensure_systemd_preferred() {
  local init_name
  if has_systemd; then
    log "systemd is active"
    return 0
  fi

  init_name="$(pid1_name)"
  if [[ "${init_name}" != "systemd" ]]; then
    if systemd_pkg_present; then
      log "systemd packages already exist, but current init is ${init_name}; use non-systemd runtime fallback"
    else
      log "PID 1 is ${init_name}; skip systemd package installation and use non-systemd runtime fallback"
    fi
    return 1
  fi

  if ! systemd_pkg_present; then
    log "systemd is not installed; start package installation"
    retry 3 3 apt_update_once
    retry 3 3 apt_install_systemd
  else
    log "systemd packages already exist but init is ${init_name}"
  fi

  if has_systemd; then
    log "systemd became active"
    return 0
  fi

  if systemd_pkg_present; then
    log "systemd packages are installed, but current init is ${init_name}; reboot or run the container with systemd as PID 1 to use systemctl"
    return 1
  fi

  log "failed to install systemd packages; continue with non-systemd fallback"
  return 1
}

configure_runtime_config() {
  cp "${CONFIG_SRC}" "${CONFIG_DST}"
  CLASH_CONFIG_PATH="${CONFIG_DST}" CLASH_BASE_URL="${BASE_URL}" GEOX_GEOIP_URL="${GEOX_GEOIP_URL}" GEOSITE_URL="${GEOSITE_URL}" MMDB_URL="${MMDB_URL}" ASN_URL="${ASN_URL}" python3 - <<'PY'
import os
from pathlib import Path
from urllib.parse import urlparse
import yaml

path = Path(os.environ["CLASH_CONFIG_PATH"])
cfg = yaml.safe_load(path.read_text()) or {}
base_url = os.environ.get("CLASH_BASE_URL", "").rstrip("/")
cfg.setdefault("external-controller", "127.0.0.1:9090")
cfg.setdefault("socks-port", 7891)
profile = cfg.get("profile") or {}
profile["store-selected"] = True
cfg["profile"] = profile
geoip = cfg.get("geoip") or {}
geoip["path"] = "/root/.config/clash/Country.mmdb"
cfg["geoip"] = geoip
geox_url = cfg.get("geox-url") or {}
geox_url["geoip"] = os.environ["GEOX_GEOIP_URL"]
geox_url["geosite"] = os.environ["GEOSITE_URL"]
geox_url["mmdb"] = os.environ["MMDB_URL"]
geox_url["asn"] = os.environ["ASN_URL"]
cfg["geox-url"] = geox_url
rule_providers = cfg.get("rule-providers") or {}
if base_url:
    for provider in rule_providers.values():
        url = provider.get("url")
        if not isinstance(url, str) or not url:
            continue
        filename = Path(urlparse(url).path).name
        if filename:
            provider["url"] = f"{base_url}/rule/{filename}"
    cfg["rule-providers"] = rule_providers
path.write_text(yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False))
PY
}

ensure_cn_direct_rules() {
  CLASH_CONFIG_PATH="${CONFIG_DST}" CLASH_BASE_URL="${BASE_URL}" python3 - <<'PY'
import os
from pathlib import Path
from urllib.parse import urlparse

import yaml

path = Path(os.environ["CLASH_CONFIG_PATH"])
cfg = yaml.safe_load(path.read_text()) or {}
rules = list(cfg.get("rules") or [])
prepend = [
    "DOMAIN-SUFFIX,mirrors.ustc.edu.cn,DIRECT",
    "DOMAIN-SUFFIX,pypi.tuna.tsinghua.edu.cn,DIRECT",
    "DOMAIN-SUFFIX,mirrors.cloud.tencent.com,DIRECT",
]
managed = set(prepend)
base_host = urlparse(os.environ.get("CLASH_BASE_URL", "")).hostname
if base_host:
    managed.add(f"DOMAIN-SUFFIX,{base_host},DIRECT")
rules = [rule for rule in rules if rule not in managed]
for rule in reversed(prepend):
    if rule in rules:
        rules.remove(rule)
    rules.insert(0, rule)
cfg["rules"] = rules
path.write_text(yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False))
PY
}

download_assets() {
  retry 5 3 curl -L --fail --output "${CLASH_DIR}/Country.mmdb" "${MMDB_URL}"
  retry 5 3 curl -L --fail --output "${GEOIP_METADB_PATH}" "${GEOIP_METADB_URL}"
  retry 5 3 curl -L --fail --output "${MIHOMO_GZ}" "${MIHOMO_URL}"
  rm -f "${MIHOMO_BIN}"
  gzip -df "${MIHOMO_GZ}"
  install -m 0755 "${MIHOMO_BIN}" /usr/local/bin/clash
}

write_service() {
  cat >"${CLASH_RUNNER}" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

mkdir -p /root/.config/mihomo/run
exec /usr/local/bin/clash -d /root/.config/mihomo -f /root/.config/clash/config.yaml
EOF
  chmod 0755 "${CLASH_RUNNER}"

  mkdir -p /etc/profile.d
  cat >/etc/profile.d/clash-autostart.sh <<'EOF'
# Lazily start Clash for containers where systemd/supervisor is not PID 1.
if [ -x /root/clash/run-clash.sh ] && command -v curl >/dev/null 2>&1; then
  if ! curl --noproxy 127.0.0.1,localhost -fsS http://127.0.0.1:9090/version >/dev/null 2>&1; then
    if ! pgrep -f '^/usr/local/bin/clash -d /root/.config/mihomo -f /root/.config/clash/config.yaml( |$)' >/dev/null 2>&1; then
      mkdir -p /root/.config/mihomo/run
      setsid -f /root/clash/run-clash.sh >>/root/.config/mihomo/run/clash.log 2>&1 < /dev/null
      sleep 1
      pgrep -f '^/usr/local/bin/clash -d /root/.config/mihomo -f /root/.config/clash/config.yaml( |$)' | tail -1 >/root/.config/mihomo/run/clash.pid
    fi
  fi
fi
EOF
  chmod 0644 /etc/profile.d/clash-autostart.sh

  if ! systemd_pkg_present; then
    return 0
  fi

  mkdir -p "$(dirname "${SERVICE_FILE}")"
  cat >"${SERVICE_FILE}" <<'EOF'
[Unit]
Description=Clash compatible core (mihomo)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/root/clash/run-clash.sh
Restart=on-failure
RestartSec=5
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
EOF
}

write_supervisor_service() {
  [[ -d "${SUPERVISOR_CONF_DIR}" ]] || return 1
  cat >"${SUPERVISOR_CONF_FILE}" <<EOF
[program:${SUPERVISOR_PROGRAM}]
command=${CLASH_RUNNER}
directory=/root
autostart=true
autorestart=true
startsecs=3
stdout_logfile=${CLASH_LOG_FILE}
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=3
redirect_stderr=true
stopasgroup=true
killasgroup=true
EOF
}

write_s6_service() {
  mkdir -p "${S6_SERVICE_DIR}"
  cat >"${S6_SERVICE_DIR}/run" <<'EOF'
#!/usr/bin/env sh
exec /root/clash/run-clash.sh
EOF
  chmod 0755 "${S6_SERVICE_DIR}/run"
  cat >"${S6_SERVICE_DIR}/finish" <<'EOF'
#!/usr/bin/env sh
exit 0
EOF
  chmod 0755 "${S6_SERVICE_DIR}/finish"
}

write_runit_service() {
  mkdir -p "${RUNIT_SERVICE_DIR}" "${RUNIT_SERVICE_LINK_DIR}"
  cat >"${RUNIT_SERVICE_DIR}/run" <<'EOF'
#!/usr/bin/env bash
exec /root/clash/run-clash.sh
EOF
  chmod 0755 "${RUNIT_SERVICE_DIR}/run"
  ln -snf "${RUNIT_SERVICE_DIR}" "${RUNIT_SERVICE_LINK_DIR}/clash"
}

write_openrc_service() {
  mkdir -p "$(dirname "${OPENRC_SERVICE_FILE}")"
  cat >"${OPENRC_SERVICE_FILE}" <<'EOF'
#!/sbin/openrc-run

name="clash"
description="Clash compatible core (mihomo)"
command="/root/clash/run-clash.sh"
command_background="yes"
pidfile="/root/.config/mihomo/run/clash.pid"
output_log="/root/.config/mihomo/run/clash.log"
error_log="/root/.config/mihomo/run/clash.log"
retry="TERM/10/KILL/5"

depend() {
  need net
}

start_pre() {
  mkdir -p /root/.config/mihomo/run
}
EOF
  chmod 0755 "${OPENRC_SERVICE_FILE}"
}

clash_pid() {
  [[ -f "${CLASH_PID_FILE}" ]] || return 1
  cat "${CLASH_PID_FILE}"
}

clash_process_pids() {
  pgrep -f "^/usr/local/bin/clash -d ${MIHOMO_DIR} -f ${CONFIG_DST}( |$)" 2>/dev/null || true
}

clash_pid_alive() {
  local pid process_pid
  pid="$(clash_pid 2>/dev/null || true)"
  if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
    return 0
  fi
  while IFS= read -r process_pid; do
    [[ -n "${process_pid}" ]] && kill -0 "${process_pid}" 2>/dev/null && return 0
  done < <(clash_process_pids)
  return 1
}

stop_clash_runtime() {
  local pid manager process_pid
  manager="$(runtime_manager)"
  case "${manager}" in
    systemd)
      systemctl stop clash >/dev/null 2>&1 || true
      return 0
      ;;
    pid1-supervisor)
      supervisorctl stop "${SUPERVISOR_PROGRAM}" >/dev/null 2>&1 || true
      return 0
      ;;
    pid1-s6)
      s6-svc -d "${S6_SERVICE_DIR}" >/dev/null 2>&1 || true
      return 0
      ;;
    pid1-runit)
      sv force-stop clash >/dev/null 2>&1 || sv stop clash >/dev/null 2>&1 || true
      return 0
      ;;
    pid1-openrc)
      rc-service clash stop >/dev/null 2>&1 || true
      return 0
      ;;
  esac
  pid="$(clash_pid 2>/dev/null || true)"
  if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
    kill "${pid}" >/dev/null 2>&1 || true
    for _ in $(seq 1 10); do
      kill -0 "${pid}" 2>/dev/null || break
      sleep 1
    done
    kill -9 "${pid}" >/dev/null 2>&1 || true
  fi
  rm -f "${CLASH_PID_FILE}"
  while IFS= read -r process_pid; do
    [[ -n "${process_pid}" ]] && kill "${process_pid}" >/dev/null 2>&1 || true
  done < <(clash_process_pids)
  sleep 1
  while IFS= read -r process_pid; do
    [[ -n "${process_pid}" ]] && kill -9 "${process_pid}" >/dev/null 2>&1 || true
  done < <(clash_process_pids)
}

start_clash_runtime() {
  local pid manager init_name
  manager="$(runtime_manager)"
  init_name="$(pid1_name)"
  case "${manager}" in
    systemd)
      retry 3 2 systemctl daemon-reload
      retry 3 2 systemctl restart clash
      return 0
      ;;
    pid1-supervisor)
      log "systemd is unavailable in current boot; use PID 1 (${init_name}) supervisor management"
      write_supervisor_service
      retry 3 2 supervisorctl reread
      retry 3 2 supervisorctl update
      retry 3 2 supervisorctl restart "${SUPERVISOR_PROGRAM}"
      return 0
      ;;
    pid1-s6)
      log "systemd is unavailable in current boot; use PID 1 (${init_name}) s6 management"
      write_s6_service
      retry 3 2 s6-svc -d "${S6_SERVICE_DIR}"
      retry 3 2 s6-svc -u "${S6_SERVICE_DIR}"
      return 0
      ;;
    pid1-runit)
      log "systemd is unavailable in current boot; use PID 1 (${init_name}) runit management"
      write_runit_service
      sv force-stop clash >/dev/null 2>&1 || sv stop clash >/dev/null 2>&1 || true
      retry 3 2 sv up clash
      return 0
      ;;
    pid1-openrc)
      log "systemd is unavailable in current boot; use PID 1 (${init_name}) openrc management"
      write_openrc_service
      rc-service clash stop >/dev/null 2>&1 || true
      retry 3 2 rc-service clash start
      return 0
      ;;
  esac
  log "PID 1 (${init_name}) does not expose a supported service manager; use direct process mode"
  if clash_pid_alive && wait_api_ready; then
    log "clash is already running in direct process mode"
    return 0
  fi
  stop_clash_runtime
  : > "${CLASH_LOG_FILE}"
  setsid -f "${CLASH_RUNNER}" >>"${CLASH_LOG_FILE}" 2>&1 < /dev/null
  sleep 1
  pid="$(clash_process_pids | tail -1)"
  printf '%s\n' "${pid}" > "${CLASH_PID_FILE}"
  if [[ -z "${pid}" ]] || ! kill -0 "${pid}" 2>/dev/null; then
    log "clash failed to start without systemd; check ${CLASH_LOG_FILE}"
    return 1
  fi
}

upsert_line() {
  local file="$1"
  local marker="$2"
  local line="$3"
  touch "${file}"
  if grep -qF "${marker}" "${file}"; then
    sed -i "s|.*${marker}.*|${line}|" "${file}"
  else
    printf '%s\n' "${line}" >> "${file}"
  fi
}

ensure_system_proxy() {
  mkdir -p /etc/profile.d
  cat >/etc/profile.d/clash-proxy.sh <<EOF
export http_proxy=${HTTP_PROXY_ADDR}
export https_proxy=${HTTP_PROXY_ADDR}
export all_proxy=${SOCKS_PROXY_ADDR}
export HTTP_PROXY=${HTTP_PROXY_ADDR}
export HTTPS_PROXY=${HTTP_PROXY_ADDR}
export ALL_PROXY=${SOCKS_PROXY_ADDR}
export no_proxy=localhost,127.0.0.1,::1
export NO_PROXY=localhost,127.0.0.1,::1
EOF
  chmod 0644 /etc/profile.d/clash-proxy.sh

  upsert_line /root/.bashrc "clash-proxy http_proxy" "export http_proxy=${HTTP_PROXY_ADDR} # clash-proxy http_proxy"
  upsert_line /root/.bashrc "clash-proxy https_proxy" "export https_proxy=${HTTP_PROXY_ADDR} # clash-proxy https_proxy"
  upsert_line /root/.bashrc "clash-proxy all_proxy" "export all_proxy=${SOCKS_PROXY_ADDR} # clash-proxy all_proxy"
  upsert_line /root/.bashrc "clash-proxy HTTP_PROXY" "export HTTP_PROXY=${HTTP_PROXY_ADDR} # clash-proxy HTTP_PROXY"
  upsert_line /root/.bashrc "clash-proxy HTTPS_PROXY" "export HTTPS_PROXY=${HTTP_PROXY_ADDR} # clash-proxy HTTPS_PROXY"
  upsert_line /root/.bashrc "clash-proxy ALL_PROXY" "export ALL_PROXY=${SOCKS_PROXY_ADDR} # clash-proxy ALL_PROXY"
  upsert_line /root/.bashrc "clash-proxy no_proxy" "export no_proxy=localhost,127.0.0.1,::1 # clash-proxy no_proxy"
  upsert_line /root/.bashrc "clash-proxy NO_PROXY" "export NO_PROXY=localhost,127.0.0.1,::1 # clash-proxy NO_PROXY"

  cat >/root/.curlrc <<EOF
proxy = "${HTTP_PROXY_ADDR}"
EOF
  cat >/root/.wgetrc <<EOF
use_proxy = on
http_proxy = ${HTTP_PROXY_ADDR}
https_proxy = ${HTTP_PROXY_ADDR}
EOF
  cat >/etc/apt/apt.conf.d/99clash-proxy <<EOF
Acquire::http::Proxy "${HTTP_PROXY_ADDR}";
Acquire::https::Proxy "${HTTP_PROXY_ADDR}";
EOF
}

configure_docker_proxy() {
  if (( SKIP_DOCKER_PROXY == 1 )); then
    log "skip docker proxy configuration by option"
    return 0
  fi
  if ! has_systemd; then
    log "systemd is unavailable, skip docker proxy configuration"
    return 0
  fi
  if ! has_docker; then
    log "docker not found, skip docker proxy configuration"
    return 0
  fi
  mkdir -p /etc/systemd/system/docker.service.d
  cat >"${DOCKER_PROXY_FILE}" <<EOF
[Service]
Environment="HTTP_PROXY=${HTTP_PROXY_ADDR}"
Environment="HTTPS_PROXY=${HTTP_PROXY_ADDR}"
Environment="ALL_PROXY=${SOCKS_PROXY_ADDR}"
Environment="NO_PROXY=localhost,127.0.0.1,::1"
EOF
  systemctl daemon-reload
  if systemctl list-unit-files 2>/dev/null | grep -q '^docker\.service'; then
    systemctl restart docker || true
  fi
  log "docker proxy configured"
}

wait_api_ready() {
  local i
  for i in $(seq 1 45); do
    if curl --noproxy 127.0.0.1,localhost -fsS "${API_URL}/version" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
  done
  return 1
}

ensure_existing_clash_api() {
  if curl --noproxy 127.0.0.1,localhost -fsS "${API_URL}/version" >/dev/null 2>&1; then
    return 0
  fi
  if has_clash; then
    start_clash_runtime || true
    retry 3 2 wait_api_ready || return 1
    return 0
  fi
  return 1
}

enable_clash_autostart() {
  local manager init_name
  manager="$(runtime_manager)"
  init_name="$(pid1_name)"
  if has_systemd && ([[ -f "${SERVICE_FILE}" ]] || systemctl list-unit-files 2>/dev/null | grep -q '^clash\.service'); then
    retry 3 2 systemctl enable clash
    return 0
  fi
  if [[ "${manager}" == "pid1-supervisor" ]]; then
    write_supervisor_service
    retry 3 2 supervisorctl reread
    retry 3 2 supervisorctl update
    return 0
  fi
  if [[ "${manager}" == "pid1-s6" ]]; then
    write_s6_service
    return 0
  fi
  if [[ "${manager}" == "pid1-runit" ]]; then
    write_runit_service
    return 0
  fi
  if [[ "${manager}" == "pid1-openrc" ]]; then
    write_openrc_service
    retry 3 2 rc-update add clash default >/dev/null 2>&1 || true
    return 0
  fi
  log "autostart is unavailable: PID 1 (${init_name}) is not a supported native service manager"
}

proxy_exists_in_config() {
  local target="$1"
  python3 - "$target" "${CONFIG_DST}" <<'PY'
import sys
from pathlib import Path
import yaml

target = sys.argv[1]
cfg = yaml.safe_load(Path(sys.argv[2]).read_text())
group_names = []
for group in cfg.get("proxy-groups", []):
    if group.get("name") == "PROXY" and isinstance(group.get("proxies"), list):
        group_names = [name for name in group.get("proxies", []) if isinstance(name, str) and name]
        break
names = set(group_names) if group_names else {p.get("name") for p in cfg.get("proxies", []) if p.get("name")}
raise SystemExit(0 if target in names else 1)
PY
}

prompt_manual_proxy_selection() {
  local parsed_file answer selected
  if ! ensure_existing_clash_api; then
    log "clash api is not available"
    return 1
  fi
  ensure_python_yaml
  parsed_file="$(mktemp)"
  python3 - "${CONFIG_DST}" "${parsed_file}" <<'PY'
import sys
from pathlib import Path
import yaml

cfg = yaml.safe_load(Path(sys.argv[1]).read_text()) or {}
names = []
for group in cfg.get("proxy-groups", []):
    if group.get("name") == "PROXY" and isinstance(group.get("proxies"), list):
        names = [name for name in group.get("proxies", []) if isinstance(name, str) and name]
        break
if not names:
    names = [p.get("name") for p in cfg.get("proxies", []) if p.get("name")]
deduped = list(dict.fromkeys(names))
Path(sys.argv[2]).write_text(
    "".join(f"{idx}\t{name}\n" for idx, name in enumerate(deduped, 1)),
    encoding="utf-8",
)
PY
  if [[ ! -s "${parsed_file}" ]]; then
    rm -f "${parsed_file}"
    log "no selectable proxies found"
    return 1
  fi
  echo "检测到已有 clash，当前可手动选择以下节点："
  while IFS=$'\t' read -r idx name; do
    printf '%3s. %s\n' "${idx}" "${name}"
  done <"${parsed_file}"
  while true; do
    read -r -p "请输入序号或节点名，直接回车取消: " answer
    if [[ -z "${answer}" ]]; then
      rm -f "${parsed_file}"
      log "manual proxy selection cancelled"
      return 1
    fi
    if [[ "${answer}" =~ ^[0-9]+$ ]]; then
      selected="$(awk -F'\t' -v idx="${answer}" '$1 == idx {$1=""; sub(/^\t/, ""); print; exit}' "${parsed_file}")"
    else
      selected="${answer}"
    fi
    if [[ -n "${selected}" ]] && proxy_exists_in_config "${selected}"; then
      break
    fi
    echo "输入无效，请重新输入。"
  done
  rm -f "${parsed_file}"
  apply_proxy_selection "${selected}"
  save_default_proxy "${selected}"
  log "switched proxy to: ${selected}"
}

apply_proxy_selection() {
  local selected="$1"
  retry 3 2 curl --noproxy 127.0.0.1,localhost -fsS -X PUT -H 'Content-Type: application/json' \
    -d "{\"name\":\"${selected}\"}" \
    "${API_URL}/proxies/PROXY" >/dev/null
}

save_default_proxy() {
  local selected="$1"
  printf '%s\n' "${selected}" > "${DEFAULT_PROXY_FILE}"
}

switch_proxy_by_name() {
  local selected="$1"
  if ! ensure_existing_clash_api; then
    log "clash api is not available"
    return 1
  fi
  ensure_python_yaml
  if ! proxy_exists_in_config "${selected}"; then
    log "proxy not found in config: ${selected}"
    return 1
  fi
  apply_proxy_selection "${selected}"
  save_default_proxy "${selected}"
  log "switched proxy to: ${selected}"
}

current_proxy_name() {
  local response=""
  response="$(curl --noproxy 127.0.0.1,localhost -fsS "${API_URL}/proxies/PROXY")" || return 1
  CURRENT_PROXY_RESPONSE="${response}" python3 - <<'PY'
import json
import os

data = json.loads(os.environ["CURRENT_PROXY_RESPONSE"])
current = data.get("now") or data.get("name") or ""
if isinstance(current, str):
    print(current)
PY
}

list_proxy_group_candidates() {
  python3 - "${CONFIG_DST}" <<'PY'
import sys
from pathlib import Path
import yaml

cfg = yaml.safe_load(Path(sys.argv[1]).read_text()) or {}
names = []
for group in cfg.get("proxy-groups", []):
    if group.get("name") == "PROXY" and isinstance(group.get("proxies"), list):
        names = [name for name in group.get("proxies", []) if isinstance(name, str) and name]
        break
if not names:
    names = [p.get("name") for p in cfg.get("proxies", []) if p.get("name")]
for name in dict.fromkeys(names):
    print(name)
PY
}

rotate_next_proxy() {
  local current="" next=""
  local candidates=()
  if ! ensure_existing_clash_api; then
    log "clash api is not available"
    return 1
  fi
  ensure_python_yaml
  mapfile -t candidates < <(list_proxy_group_candidates)
  if (( ${#candidates[@]} == 0 )); then
    log "no selectable proxies found"
    return 1
  fi
  current="$(current_proxy_name || true)"
  if [[ -z "${current}" ]]; then
    next="${candidates[0]}"
  else
    local idx
    next="${candidates[0]}"
    for idx in "${!candidates[@]}"; do
      if [[ "${candidates[idx]}" == "${current}" ]]; then
        next="${candidates[$(((idx + 1) % ${#candidates[@]}))]}"
        break
      fi
    done
  fi
  if [[ -n "${current}" && "${current}" == "${next}" && ${#candidates[@]} -gt 1 ]]; then
    next="${candidates[1]}"
  fi
  if [[ -z "${next}" ]]; then
    log "failed to determine next proxy"
    return 1
  fi
  apply_proxy_selection "${next}"
  save_default_proxy "${next}"
  if [[ -n "${current}" ]]; then
    log "rotated proxy: ${current} -> ${next}"
  else
    log "rotated proxy to: ${next}"
  fi
}

auto_select_fastest_proxy() {
  local speedtest_output selected selected_delay selected_scope selected_region top_lines summary_line parsed_file
  speedtest_output="$(PREFER_REGIONS="${PREFER_REGIONS}" CLASH_CONFIG_PATH="${CONFIG_DST}" CLASH_API_URL="${API_URL}" CLASH_AVOID_NAMES="${CLASH_AVOID_NAMES:-hk-直连(大流量),hk-cn2专线,gz专线}" python3 - <<'PY'
from concurrent.futures import ThreadPoolExecutor, as_completed
import json
import os
import sys
import urllib.parse
import urllib.request
from pathlib import Path

import yaml

prefer_regions = [item.strip().upper() for item in os.environ.get("PREFER_REGIONS", "HK,TW,JP").split(",") if item.strip()]
avoid_names = [item.strip() for item in os.environ.get("CLASH_AVOID_NAMES", "").split(",") if item.strip()]
max_nodes = int(os.environ.get("CLASH_SPEEDTEST_MAX_NODES", "0"))
workers = int(os.environ.get("CLASH_SPEEDTEST_WORKERS", "16"))
api_timeout = float(os.environ.get("CLASH_SPEEDTEST_API_TIMEOUT", "4"))
delay_timeout_ms = int(os.environ.get("CLASH_SPEEDTEST_DELAY_TIMEOUT_MS", "4000"))
api_url = os.environ.get("CLASH_API_URL", "http://127.0.0.1:9090").rstrip("/")
cfg = yaml.safe_load(Path(os.environ.get("CLASH_CONFIG_PATH", "/root/.config/clash/config.yaml")).read_text())

proxy_group_nodes = []
for group in cfg.get("proxy-groups", []):
    if group.get("name") == "PROXY" and isinstance(group.get("proxies"), list):
        proxy_group_nodes = [name for name in group.get("proxies", []) if isinstance(name, str) and name]
        break
nodes = list(dict.fromkeys(proxy_group_nodes or [p["name"] for p in cfg.get("proxies", []) if p.get("name")]))
results = []

def normalize(value: str) -> str:
    return "".join(ch for ch in value.lower() if not ch.isspace() and ch not in "()[]-_")

normalized_avoid = [normalize(item) for item in avoid_names]

def match_region(name: str) -> str:
    lower = name.lower()
    for region in prefer_regions:
        if region in {"", "NONE", "NO", "0"}:
            continue
        if region == "HK":
            if any(token in name for token in ("香港", "港")) or any(token in lower for token in ("hong kong", "hk", "hkg")):
                return "HK"
        elif region == "TW":
            if any(token in name for token in ("台湾", "台")) or any(token in lower for token in ("taiwan", "taipei", "tw", "tpe")):
                return "TW"
        elif region == "JP":
            if "日本" in name or any(token in lower for token in ("japan", "tokyo", "osaka", "chiba", "jp")):
                return "JP"
        elif region.lower() in lower:
            return region
    return ""

def is_avoided(name: str) -> bool:
    normalized_name = normalize(name)
    return any(pattern and pattern in normalized_name for pattern in normalized_avoid)

test_nodes = [name for name in nodes if not is_avoided(name)]
if max_nodes > 0:
    test_nodes = test_nodes[:max_nodes]
preferred_nodes = [name for name in test_nodes if match_region(name)]

def test_delay(name: str):
    enc = urllib.parse.quote(name, safe="")
    url = f"{api_url}/proxies/{enc}/delay?timeout={delay_timeout_ms}&url=http://1.1.1.1"
    opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
    try:
        with opener.open(url, timeout=api_timeout) as response:
            delay = json.loads(response.read().decode()).get("delay")
    except Exception:
        return None
    if not isinstance(delay, int):
        return None
    region = match_region(name)
    return {"name": name, "delay": delay, "preferred": bool(region), "region": region}

print(
    f"speedtest nodes={len(test_nodes)} preferred_candidates={len(preferred_nodes)} "
    f"excluded={len(nodes) - len(test_nodes)} workers={workers}",
    file=sys.stderr,
)
if not test_nodes:
    print("no eligible proxy candidates after exclusions", file=sys.stderr)
    raise SystemExit(1)
with ThreadPoolExecutor(max_workers=max(1, workers)) as executor:
    futures = [executor.submit(test_delay, name) for name in test_nodes]
    for future in as_completed(futures):
        item = future.result()
        if item is not None:
            results.append(item)

if not results:
    print("no valid proxy delay results", file=sys.stderr)
    raise SystemExit(1)

preferred = sorted([item for item in results if item["preferred"]], key=lambda item: item["delay"])
fallback = sorted(results, key=lambda item: item["delay"])
pool = preferred or fallback
chosen = pool[0]
if chosen in preferred:
    scope = "preferred"
else:
    scope = "fallback"
top = sorted(results, key=lambda item: (item["delay"], item["name"]))[:12]
summary = {
    "total_candidates": len(nodes),
    "attempted_candidates": len(test_nodes),
    "successful_candidates": len(results),
    "preferred_candidates": len(preferred_nodes),
    "preferred_success": len(preferred),
    "excluded_candidates": len(nodes) - len(test_nodes),
    "selected": chosen["name"],
    "selected_delay": chosen["delay"],
    "selected_scope": scope,
    "selected_region": chosen["region"] or "GLOBAL",
    "top": top,
}
print(json.dumps(summary, ensure_ascii=False))
PY
)"
  [[ -n "${speedtest_output}" ]] || return 1
  parsed_file="$(mktemp)"
  SPEEDTEST_OUTPUT="${speedtest_output}" python3 - "${parsed_file}" <<'PY'
import json
import os
import sys
from pathlib import Path

data = json.loads(os.environ["SPEEDTEST_OUTPUT"])
out = Path(sys.argv[1])
lines = [
    f"SELECTED={data['selected']}",
    f"SELECTED_DELAY={data['selected_delay']}",
    f"SELECTED_SCOPE={data['selected_scope']}",
    f"SELECTED_REGION={data['selected_region']}",
    f"SUMMARY=total_candidates={data['total_candidates']} attempted={data['attempted_candidates']} successful={data['successful_candidates']} preferred_success={data['preferred_success']} preferred_candidates={data['preferred_candidates']} excluded_candidates={data['excluded_candidates']}",
]
for i, item in enumerate(data["top"], 1):
    pref = "yes" if item["preferred"] else "no"
    region = item["region"] or "GLOBAL"
    lines.append(f"TOP={i}. {item['delay']}ms | {item['name']} | preferred={pref} | region={region}")
out.write_text("\n".join(lines) + "\n")
PY
  selected="$(sed -n 's/^SELECTED=//p' "${parsed_file}")"
  selected_delay="$(sed -n 's/^SELECTED_DELAY=//p' "${parsed_file}")"
  selected_scope="$(sed -n 's/^SELECTED_SCOPE=//p' "${parsed_file}")"
  selected_region="$(sed -n 's/^SELECTED_REGION=//p' "${parsed_file}")"
  summary_line="$(sed -n 's/^SUMMARY=//p' "${parsed_file}")"
  top_lines="$(sed -n 's/^TOP=//p' "${parsed_file}")"
  rm -f "${parsed_file}"
  [[ -n "${selected}" ]] || return 1
  apply_proxy_selection "${selected}"
  save_default_proxy "${selected}"
  log "speedtest summary: ${summary_line}"
  while IFS= read -r line; do
    [[ -n "${line}" ]] && log "top candidate: ${line}"
  done <<<"${top_lines}"
  log "selected scope: ${selected_scope}"
  log "selected region: ${selected_region}"
  log "selected proxy: $(highlight_selected "${selected}")"
  log "selected delay: $(highlight_selected "${selected_delay}ms")"
}

deploy_clash() {
  ensure_dirs
  require_config
  ensure_python_yaml
  configure_runtime_config
  ensure_cn_direct_rules
  download_assets
  write_service
  retry 3 2 /usr/local/bin/clash -t -d "${MIHOMO_DIR}" -f "${CONFIG_DST}"
  enable_clash_autostart >/dev/null 2>&1 || true
  start_clash_runtime
  retry 3 2 wait_api_ready
  ensure_system_proxy
}

choose_continue_if_clash_exists() {
  if ! has_clash; then
    return 0
  fi
  if (( FORCE == 1 || NON_INTERACTIVE == 1 )); then
    log "existing clash detected; overwrite enabled"
    return 0
  fi
  echo "检测到本机已存在 clash。"
  read -r -p "是否继续部署/覆盖当前 clash 配置? [y/N]: " answer
  case "${answer:-N}" in
    y|Y|yes|YES)
      return 0
      ;;
    *)
      log "user chose not to overwrite existing clash"
      return 10
      ;;
  esac
}

main() {
  local existing_action_rc
  parse_args "$@"
  init_colors
  mark_apt_state "pending"
  log_runtime_manager_diagnostics "initial"
  ensure_systemd_preferred || true
  log_runtime_manager_diagnostics "post-systemd-prepare"
  if (( ROTATE_NEXT == 1 )); then
    rotate_next_proxy
    finalize_apt_state_success
    exit 0
  fi
  if [[ -n "${TARGET_PROXY_NAME}" && has_clash ]]; then
    if ensure_existing_clash_api; then
      log "existing clash detected; switch proxy only"
      switch_proxy_by_name "${TARGET_PROXY_NAME}"
      finalize_apt_state_success
      echo
      echo "current PROXY group:"
      curl --noproxy 127.0.0.1,localhost -fsS "${API_URL}/proxies/PROXY" || true
      exit 0
    fi
    log "existing clash detected but api is unavailable; continue with clash refresh before switching proxy"
  fi
  if choose_continue_if_clash_exists; then
    :
  else
    existing_action_rc="$?"
    case "${existing_action_rc}" in
      10)
        enable_clash_autostart >/dev/null 2>&1 || true
        configure_docker_proxy
        prompt_manual_proxy_selection || true
        finalize_apt_state_success
        echo
        echo "current PROXY group:"
        curl --noproxy 127.0.0.1,localhost -fsS "${API_URL}/proxies/PROXY" || true
        exit 0
        ;;
      *)
        exit "${existing_action_rc}"
        ;;
    esac
  fi
  if ! has_clash; then
    log "clash not found, start automatic deployment"
  else
    log "clash exists, refresh binary and config"
  fi
  deploy_clash
  configure_docker_proxy
  if [[ -n "${TARGET_PROXY_NAME}" ]]; then
    switch_proxy_by_name "${TARGET_PROXY_NAME}"
  elif (( AUTO_SELECT == 1 )); then
    log "start proxy delay test; prefer regions=${PREFER_REGIONS}"
    auto_select_fastest_proxy || log "proxy auto selection failed; keep current selection"
  fi
  finalize_apt_state_success
  echo "clash version:"
  clash -v || true
  echo
  echo "current PROXY group:"
  curl --noproxy 127.0.0.1,localhost -fsS "${API_URL}/proxies/PROXY" || true
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi
