#!/bin/sh

PACKAGE="${NASTUNNEL_PACKAGE:-nastunnel}"
DNAME="NAS Tunnel"
INSTALL_DIR="${NASTUNNEL_INSTALL_DIR:-/var/packages/${PACKAGE}/target}"
SCRIPT_DIR="${NASTUNNEL_SCRIPT_DIR:-/var/packages/${PACKAGE}/scripts}"
SCRIPT_PATH="${SCRIPT_DIR}/start-stop-status"
NPC_BIN="${INSTALL_DIR}/bin/npc"
CONFIG_FILE="${INSTALL_DIR}/conf/config.json"
PID_FILE="${INSTALL_DIR}/var/npc.pid"
PID_DIR="${INSTALL_DIR}/var/npc_pids"
LOG_FILE="${INSTALL_DIR}/var/npc.log"
MONITOR_PID_FILE="${INSTALL_DIR}/var/monitor.pid"
GUARDIAN_PID_FILE="${INSTALL_DIR}/var/guardian.pid"
LAST_SERVERS_FILE="${INSTALL_DIR}/var/last_servers"
LAST_VKEY_FILE="${INSTALL_DIR}/var/last_vkey"
LAST_NETWORK_REPORT_FILE="${INSTALL_DIR}/var/last_network_report_epoch"
LAST_NETWORK_REPORT_TIME_FILE="${INSTALL_DIR}/var/last_report_time"
DNS_ENABLED_MARKER="${INSTALL_DIR}/var/aliyun_dns_enabled"
DNS_HELPER="${INSTALL_DIR}/bin/config_dns.sh"
STOP_FLAG="${INSTALL_DIR}/var/stopped"
SYNC_LOCK_DIR="${INSTALL_DIR}/var/sync.lock"
MONITOR_START_LOCK_DIR="${INSTALL_DIR}/var/monitor_start.lock"
GUARDIAN_START_LOCK_DIR="${INSTALL_DIR}/var/guardian_start.lock"
MONITOR_RUNTIME_LOCK_DIR="${INSTALL_DIR}/var/monitor_runtime.lock"
GUARDIAN_RUNTIME_LOCK_DIR="${INSTALL_DIR}/var/guardian_runtime.lock"
AUTO_START_DISABLED_MARKER="${INSTALL_DIR}/var/auto_start_disabled"
SYSTEMD_GUARDIAN_MARKER="${NASTUNNEL_SYSTEMD_GUARDIAN_MARKER:-/var/packages/${PACKAGE}/var/systemd_guardian_enabled}"
SMART_DNS_API_URL="${NASTUNNEL_SMART_DNS_API_URL:-http://nas.8x6x.com/api/smartdnsclientservers}"
SMART_DNS_API_URL_IP="${NASTUNNEL_SMART_DNS_API_URL_IP:-http://116.131.157.27/api/smartdnsclientservers}"
PUBLIC_IP_URL="${NASTUNNEL_PUBLIC_IP_URL:-http://4.ipw.cn}"
CLIENT_TYPE="synology"
CLIENT_NAME="Synology%20Client"
CLIENT_VERSION="7.5"
CLIENT_PLATFORM="Synology%2FDSM"
CHECK_INTERVAL="${NASTUNNEL_CHECK_INTERVAL:-60}"
GUARDIAN_INTERVAL="${NASTUNNEL_GUARDIAN_INTERVAL:-15}"
NETWORK_REPORT_INTERVAL="${NASTUNNEL_NETWORK_REPORT_INTERVAL:-600}"
FAILURE_LOG_INTERVAL="${NASTUNNEL_FAILURE_LOG_INTERVAL:-1800}"

log_msg() {
    mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
    { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; } 2>/dev/null || true
    return 0
}

log_msg_limited() {
    key=$(echo "$1" | sed 's/[^A-Za-z0-9_-]/_/g' | cut -c 1-80)
    interval="$2"
    message="$3"
    stamp="${INSTALL_DIR}/var/log_${key}.stamp"
    now=$(date +%s)
    last=$(cat "$stamp" 2>/dev/null)
    case "$last" in
        ''|*[!0-9]*) last=0 ;;
    esac
    if [ $((now - last)) -ge "$interval" ]; then
        log_msg "$message"
        printf '%s\n' "$now" > "${stamp}.tmp" 2>/dev/null &&
            mv "${stamp}.tmp" "$stamp" 2>/dev/null
    fi
    return 0
}

clear_limited_log() {
    key=$(echo "$1" | sed 's/[^A-Za-z0-9_-]/_/g' | cut -c 1-80)
    rm -f "${INSTALL_DIR}/var/log_${key}.stamp" 2>/dev/null
}

json_get() {
    key="$1"
    if [ -f "$CONFIG_FILE" ]; then
        grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$CONFIG_FILE" | head -1 | cut -d'"' -f4
    fi
}

json_get_bool() {
    key="$1"
    if [ -f "$CONFIG_FILE" ]; then
        grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\\(true\\|false\\)" "$CONFIG_FILE" | head -1 | sed 's/.*:[[:space:]]*//'
    fi
}

url_encode() {
    echo -n "$1" | sed 's/ /%20/g; s/!/%21/g; s/"/%22/g; s/#/%23/g; s/\$/%24/g; s/&/%26/g; s/'\''/%27/g; s/(/%28/g; s/)/%29/g; s/*/%2A/g; s/+/%2B/g; s/,/%2C/g; s/:/%3A/g; s/;/%3B/g; s/=/%3D/g; s/?/%3F/g; s/@/%40/g'
}

safe_server_name() {
    name=$(echo -n "$1" | sed 's/[^A-Za-z0-9]/_/g; s/^_*//; s/_*$//')
    if [ -z "$name" ]; then
        name="default"
    fi
    echo "$name" | cut -c 1-120
}

pid_file_for_server() {
    echo "${PID_DIR}/npc_$(safe_server_name "$1").pid"
}

server_endpoint_key() (
    server="$1"
    host="${server%:*}"
    port="${server##*:}"
    lower_host=$(printf '%s' "$host" | tr 'A-Z' 'a-z')
    resolved=""
    case "$lower_host" in
        \[*\])
            # A bracketed IPv6 literal must not be sent through DNS.
            resolved="$lower_host"
            ;;
        *[!0-9.]*)
            resolved=$(nslookup "$host" 2>/dev/null | awk '
                /^Name:/ { answer=1; next }
                answer && $1 ~ /^Address(es)?:$/ {
                    address=$2
                    if (address ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
                        print address
                        exit
                    }
                }
                answer && $1 == "Address" && $2 ~ /^[0-9]+:$/ {
                    address=$3
                    if (address ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
                        print address
                        exit
                    }
                }
            ')
            if [ -z "$resolved" ]; then
                resolved=$(ping -c 1 -W 1 "$host" 2>/dev/null | sed -n '1s/.*(\([^)]*\)).*/\1/p')
            fi
            ;;
        *)
            resolved="$lower_host"
            ;;
    esac
    [ -n "$resolved" ] || resolved="$lower_host"
    printf '%s:%s\n' "$resolved" "$port"
)

deduplicate_servers() (
    # PID files deliberately retain their legacy domain-based names. Reconcile
    # API entries to live processes by endpoint first, so changing an alias or
    # API ordering never tears down an otherwise healthy MUX connection.
    # Use one DNS result per name in this pass, including the running aliases.
    input="$1"
    running=$(discover_running_servers)
    endpoint_map=$(
        printf '%s\n%s\n' "$input" "$running" | awk 'NF && !seen[$0]++' |
        while IFS= read -r address; do
            printf '%s\t%s\n' "$address" "$(server_endpoint_key "$address")"
        done
    )
    result=""
    seen_keys="|"
    for server in $input; do
        key=$(printf '%s\n' "$endpoint_map" | awk -v address="$server" '$1 == address { print $2; exit }')
        case "$seen_keys" in
            *"|${key}|"*) continue ;;
        esac
        seen_keys="${seen_keys}${key}|"
        preferred="$server"
        for live_server in $running; do
            live_key=$(printf '%s\n' "$endpoint_map" | awk -v address="$live_server" '$1 == address { print $2; exit }')
            if [ "$live_key" = "$key" ]; then
                preferred="$live_server"
                break
            fi
        done
        result="${result}
${preferred}"
    done
    printf '%s\n' "$result" | awk 'NF'
)

pid_running() {
    pid="$1"
    [ -n "$pid" ] || return 1
    err=$(kill -0 "$pid" 2>&1)
    rc=$?
    if [ "$rc" = "0" ]; then
        return 0
    fi
    echo "$err" | grep -qi "Operation not permitted" && return 0
    return 1
}

terminate_pid() {
    pid="$1"
    pid_running "$pid" || return 0
    kill "$pid" 2>/dev/null || true
    attempts=0
    while pid_running "$pid" && [ "$attempts" -lt 5 ]; do
        sleep 1
        attempts=$((attempts + 1))
    done
    if pid_running "$pid"; then
        kill -9 "$pid" 2>/dev/null || true
    fi
}

process_args() {
    pid="$1"
    if [ -r "/proc/${pid}/cmdline" ] && tr '\000' '\n' < "/proc/${pid}/cmdline" 2>/dev/null; then
        return 0
    fi
    ps -o args= -p "$pid" 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) print $i }'
}

npc_pid_running() {
    pid="$1"
    server="$2"
    pid_running "$pid" || return 1
    exe=$(readlink -f "/proc/${pid}/exe" 2>/dev/null)
    expected_exe=$(readlink -f "$NPC_BIN" 2>/dev/null)
    [ -n "$exe" ] && [ -n "$expected_exe" ] && [ "$exe" = "$expected_exe" ] || return 1
    args=$(process_args "$pid") || return 1
    if [ -z "$args" ]; then
        [ -d "/proc/${pid}" ]
        return $?
    fi
    echo "$args" | grep -Fx "$NPC_BIN" > /dev/null 2>&1 || return 1
    [ -z "$server" ] || echo "$args" | grep -Fx -- "-server=${server}" > /dev/null 2>&1
}

monitor_running() {
    [ -f "$MONITOR_PID_FILE" ] || return 1
    pid=$(cat "$MONITOR_PID_FILE" 2>/dev/null)
    pid_running "$pid" || return 1
    args=$(process_args "$pid") || return 1
    if [ -z "$args" ]; then
        [ -d "/proc/${pid}" ]
        return $?
    fi
    echo "$args" | grep -Fx "$SCRIPT_PATH" > /dev/null 2>&1 || return 1
    echo "$args" | grep -Fx "monitor" > /dev/null 2>&1
}

guardian_running() {
    [ -f "$GUARDIAN_PID_FILE" ] || return 1
    pid=$(cat "$GUARDIAN_PID_FILE" 2>/dev/null)
    pid_running "$pid" || return 1
    args=$(process_args "$pid") || return 1
    if [ -z "$args" ]; then
        [ -d "/proc/${pid}" ]
        return $?
    fi
    echo "$args" | grep -Fx "$SCRIPT_PATH" > /dev/null 2>&1 || return 1
    echo "$args" | grep -Fx "guardian" > /dev/null 2>&1
}

acquire_sync_lock() {
    mkdir -p "$(dirname "$SYNC_LOCK_DIR")"
    if mkdir "$SYNC_LOCK_DIR" 2>/dev/null; then
        return 0
    fi

    if ! find "$SYNC_LOCK_DIR" -prune -mmin +2 2>/dev/null | grep -q .; then
        return 1
    fi
    rmdir "$SYNC_LOCK_DIR" 2>/dev/null || return 1
    mkdir "$SYNC_LOCK_DIR" 2>/dev/null || return 1
}

release_sync_lock() {
    rmdir "$SYNC_LOCK_DIR" 2>/dev/null
}

acquire_runtime_lock() {
    lock_dir="$1"
    if mkdir "$lock_dir" 2>/dev/null; then
        printf '%s\n' "$$" > "${lock_dir}/pid"
        return 0
    fi
    lock_owner=$(cat "${lock_dir}/pid" 2>/dev/null)
    if [ -n "$lock_owner" ] && pid_running "$lock_owner"; then
        return 1
    fi
    rm -f "${lock_dir}/pid" 2>/dev/null
    rmdir "$lock_dir" 2>/dev/null || return 1
    mkdir "$lock_dir" 2>/dev/null || return 1
    printf '%s\n' "$$" > "${lock_dir}/pid"
    return 0
}

release_runtime_lock() {
    lock_dir="$1"
    lock_owner=$(cat "${lock_dir}/pid" 2>/dev/null)
    if [ "$lock_owner" = "$$" ]; then
        rm -f "${lock_dir}/pid" 2>/dev/null
        rmdir "$lock_dir" 2>/dev/null
    fi
}

read_config() {
    username=$(json_get "username")
    password=$(json_get "password")
    client_id=$(json_get "client_id")
    verify_key=$(json_get "verify_key")
    config_server=$(json_get "server")
    auto_start=$(json_get_bool "auto_start")
    [ -n "$auto_start" ] || auto_start="true"
}

fetch_smartdns_servers() {
    API_SERVERS=""
    API_VERIFY_KEY=""
    if [ -z "$username" ] || [ -z "$password" ] || [ -z "$client_id" ]; then
        return 1
    fi

    encoded_user=$(url_encode "$username")
    encoded_pass=$(url_encode "$password")
    encoded_client=$(url_encode "$client_id")
    for base_url in "$SMART_DNS_API_URL" "$SMART_DNS_API_URL_IP"; do
        response=$(curl -s --connect-timeout 10 --max-time 15 "${base_url}?username=${encoded_user}&password=${encoded_pass}&client_id=${encoded_client}&client_type=${CLIENT_TYPE}&client_name=${CLIENT_NAME}&client_version=${CLIENT_VERSION}&platform=${CLIENT_PLATFORM}")
        if [ -z "$response" ]; then
            continue
        fi
        status=$(echo "$response" | grep -o '"status"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$')
        if [ "$status" != "1" ]; then
            continue
        fi
        API_VERIFY_KEY=$(echo "$response" | grep -o '"verify_key"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4)
        primary=$(echo "$response" | grep -o '"primary"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4)
        addrs=$(echo "$response" | grep -o '"addr"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
        API_SERVERS=$(printf '%s\n%s\n' "$primary" "$addrs" | awk 'NF && !seen[$0]++')
        if [ -n "$API_SERVERS" ]; then
            return 0
        fi
    done
    return 1
}

report_network_info() {
    [ -n "$client_id" ] || return 0
    now=$(date +%s)
    last=$(cat "$LAST_NETWORK_REPORT_FILE" 2>/dev/null)
    case "$last" in
        ''|*[!0-9]*) last=0 ;;
    esac
    if [ $((now - last)) -lt "$NETWORK_REPORT_INTERVAL" ]; then
        return 0
    fi

    # Report the IPv4 address selected by the default route. This avoids
    # Docker bridge, link-local and other virtual interface addresses.
    private_ips=$(ip route get 223.5.5.5 2>/dev/null | awk '
        {
            for (i = 1; i <= NF; i++) {
                if ($i == "src" && (i + 1) <= NF) {
                    print $(i + 1)
                    exit
                }
            }
        }
    ')
    case "$private_ips" in
        10.*|192.168.*|172.1[6-9].*|172.2[0-9].*|172.3[01].*) ;;
        *) private_ips="" ;;
    esac
    if [ -z "$private_ips" ]; then
        private_ips=$(ip -4 -o addr show scope global 2>/dev/null | awk '
            $2 !~ /^(docker|veth|br-|virbr|tun|tap|wg|tailscale|zerotier|lo)/ {
                address=$4
                sub(/\/.*/, "", address)
                if (address ~ /^10\./ ||
                    address ~ /^192\.168\./ ||
                    address ~ /^172\.(1[6-9]|2[0-9]|3[01])\./) {
                    print address
                    exit
                }
            }
        ')
    fi

    # Report exactly one stable public IPv6. Prefer a normal /64 address and
    # a modified EUI-64 address over a temporary/privacy or short-lived /128
    # address. Exclude Docker and other virtual interfaces.
    ipv6_addr=$(ip -6 -o addr show scope global 2>/dev/null | awk '
        $2 !~ /^(docker|veth|br-|virbr|tun|tap|wg|tailscale|zerotier|lo)/ {
            address=$4
            split(address, cidr, "/")
            address=cidr[1]
            prefix=cidr[2] + 0
            lower=tolower(address)
            if (address == "" || lower ~ /^fe80:/ || lower ~ /^f[cd]/) next

            score=0
            if (prefix > 0 && prefix <= 64) score += 100
            else if (prefix > 64 && prefix < 128) score += 40
            else if (prefix == 128) score -= 20
            if (lower ~ /ff:fe/) score += 30
            for (i = 5; i <= NF; i++) {
                if ($i == "temporary") score -= 200
                if ($i == "deprecated") score -= 100
            }
            if (!found || score > best_score || (score == best_score && address < best)) {
                found=1
                best_score=score
                best=address
            }
        }
        END {
            if (found) print best
        }
    ')
    if [ -z "$ipv6_addr" ]; then
        ipv6_addr=$(ip -6 -o addr show scope global 2>/dev/null | awk '
            {
                address=$4
                lower=tolower(address)
                if (lower ~ /^fe80:/ || lower ~ /^f[cd]/) next
                sub(/\/.*/, "", address)
                print address
                exit
            }
        ')
    fi
    ipv6_addresses="$ipv6_addr"

    public_ip=""
    for public_ip_url in "$PUBLIC_IP_URL" "https://api.ipify.org" "https://checkip.amazonaws.com"; do
        candidate=$(curl -4 -s --connect-timeout 4 --max-time 6 "$public_ip_url" | tr -d '\r\n ')
        if echo "$candidate" | awk -F. '
            NF == 4 {
                for (i = 1; i <= 4; i++) {
                    if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1
                }
                if ($1 == 10 || $1 == 127 || ($1 == 169 && $2 == 254) ||
                    ($1 == 172 && $2 >= 16 && $2 <= 31) ||
                    ($1 == 192 && $2 == 168)) exit 1
                exit 0
            }
            { exit 1 }
        '; then
            public_ip="$candidate"
            break
        fi
    done
    encoded_ipv6=$(url_encode "$ipv6_addr")
    encoded_ipv6_addresses=$(url_encode "$ipv6_addresses")
    encoded_private_ips=$(url_encode "$private_ips")
    encoded_public_ip=$(url_encode "$public_ip")
    for base_url in "$SMART_DNS_API_URL" "$SMART_DNS_API_URL_IP"; do
        response=$(curl -s --connect-timeout 10 --max-time 15 \
            "${base_url%/smartdnsclientservers}/clientnetwork?client_id=${encoded_client}&private_ips=${encoded_private_ips}&public_ip=${encoded_public_ip}&ipv6_addr=${encoded_ipv6}&ipv6_addresses=${encoded_ipv6_addresses}&client_type=${CLIENT_TYPE}&client_name=${CLIENT_NAME}&client_version=${CLIENT_VERSION}&platform=${CLIENT_PLATFORM}")
        status=$(echo "$response" | grep -o '"status"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$')
        if [ "$status" = "1" ]; then
            printf '%s\n' "$now" > "${LAST_NETWORK_REPORT_FILE}.tmp" 2>/dev/null &&
                mv "${LAST_NETWORK_REPORT_FILE}.tmp" "$LAST_NETWORK_REPORT_FILE" 2>/dev/null
            date '+%Y-%m-%d %H:%M:%S' > "${LAST_NETWORK_REPORT_TIME_FILE}.tmp" 2>/dev/null &&
                mv "${LAST_NETWORK_REPORT_TIME_FILE}.tmp" "$LAST_NETWORK_REPORT_TIME_FILE" 2>/dev/null
            clear_limited_log "network_report_failed"
            return 0
        fi
    done
    log_msg_limited "network_report_failed" "$FAILURE_LOG_INTERVAL" "network info report failed: client_id=${client_id}"
    return 0
}

cache_server_config() {
    servers="$1"
    vkey="$2"
    mkdir -p "$(dirname "$LAST_SERVERS_FILE")"
    umask 077
    printf '%s\n' "$servers" | awk 'NF && !seen[$0]++' > "${LAST_SERVERS_FILE}.tmp"
    printf '%s' "$vkey" > "${LAST_VKEY_FILE}.tmp"
    mv "${LAST_SERVERS_FILE}.tmp" "$LAST_SERVERS_FILE"
    mv "${LAST_VKEY_FILE}.tmp" "$LAST_VKEY_FILE"
}

discover_running_servers() {
    for pid_path in "$PID_DIR"/npc_*.pid; do
        [ -f "$pid_path" ] || continue
        pid=$(cat "$pid_path" 2>/dev/null)
        npc_pid_running "$pid" "" || continue
        process_args "$pid" | sed -n 's/^-server=//p'
    done | awk 'NF && !seen[$0]++'
}

start_line() {
    server="$1"
    vkey="$2"
    [ -z "$server" ] && return 1
    [ -z "$vkey" ] && return 1

    mkdir -p "$PID_DIR"
    mkdir -p "$(dirname "$PID_FILE")"
    line_pid_file=$(pid_file_for_server "$server")
    if [ -f "$line_pid_file" ] && npc_pid_running "$(cat "$line_pid_file" 2>/dev/null)" "$server"; then
        return 0
    fi
    if [ -f "$line_pid_file" ]; then
        rm -f "$line_pid_file"
    fi

    nohup "$NPC_BIN" -server="$server" -vkey="$vkey" -type=tcp > /dev/null 2>&1 &
    pid=$!
    echo "$pid" > "$line_pid_file"
    echo "$pid" > "$PID_FILE"
    sleep 1
    if npc_pid_running "$pid" "$server"; then
        log_msg "npc line started: ${server}, pid=${pid}"
        return 0
    fi
    rm -f "$line_pid_file"
    log_msg "npc line failed to start: ${server}, pid=${pid}"
    return 1
}

stop_stale_lines() {
    active_servers="$1"
    mkdir -p "$PID_DIR"
    for pid_path in "$PID_DIR"/npc_*.pid; do
        [ -f "$pid_path" ] || continue
        keep=0
        for server in $active_servers; do
            if [ "$pid_path" = "$(pid_file_for_server "$server")" ]; then
                keep=1
                break
            fi
        done
        if [ "$keep" = "0" ]; then
            pid=$(cat "$pid_path" 2>/dev/null)
            if npc_pid_running "$pid" ""; then
                kill "$pid" 2>/dev/null
            fi
            rm -f "$pid_path"
            log_msg "npc stale line stopped: ${pid_path}"
        fi
    done
}

sync_npc_lines_inner() {
    read_config
    if [ -z "$client_id" ]; then
        log_msg "waiting for login, no client_id yet"
        return 0
    fi

    if fetch_smartdns_servers; then
        servers=$(deduplicate_servers "$API_SERVERS")
        vkey="$API_VERIFY_KEY"
        [ -z "$vkey" ] && vkey="$verify_key"
        [ -z "$vkey" ] && vkey="$client_id"
        stop_stale_lines "$servers"
        cache_server_config "$servers" "$vkey"
        clear_limited_log "smartdns_api_unavailable"
    else
        if [ -s "$LAST_SERVERS_FILE" ]; then
            servers=$(cat "$LAST_SERVERS_FILE")
            vkey=$(cat "$LAST_VKEY_FILE" 2>/dev/null)
        else
            running_servers=$(discover_running_servers)
            servers=$(printf '%s\n%s\n' "$config_server" "$running_servers" | awk 'NF && !seen[$0]++')
            vkey="$verify_key"
            [ -n "$servers" ] && cache_server_config "$servers" "$vkey"
        fi
        [ -z "$vkey" ] && vkey="$client_id"
        log_msg_limited "smartdns_api_unavailable" "$FAILURE_LOG_INTERVAL" "smartdns api unavailable, keep and recover cached server lines"
        [ -z "$servers" ] && servers="vip.8x6x.com:8024"
        # Apply the same identity rule to cached/API-failure recovery too.
        servers=$(deduplicate_servers "$servers")
    fi

    [ -z "$servers" ] && servers="vip.8x6x.com:8024"
    for server in $servers; do
        start_line "$server" "$vkey"
    done
    encoded_client=$(url_encode "$client_id")
    report_network_info
    return 0
}

sync_npc_lines() {
    # When boot auto-start is disabled the DSM package and its management UI
    # must stay available, but the monitor must not silently reconnect NPC.
    # The manual "connect" action removes this marker before synchronizing.
    if [ -f "$AUTO_START_DISABLED_MARKER" ]; then
        return 0
    fi
    acquire_sync_lock || return 0
    sync_npc_lines_inner
    rc=$?
    release_sync_lock
    return "$rc"
}

stop_monitor() {
    if [ -f "$MONITOR_PID_FILE" ]; then
        pid=$(cat "$MONITOR_PID_FILE" 2>/dev/null)
        if monitor_running; then
            terminate_pid "$pid"
        fi
        rm -f "$MONITOR_PID_FILE"
    fi
}

start_monitor() {
    if ! mkdir "$MONITOR_START_LOCK_DIR" 2>/dev/null; then
        sleep 1
        monitor_running && return 0
        rmdir "$MONITOR_START_LOCK_DIR" 2>/dev/null || return 0
        mkdir "$MONITOR_START_LOCK_DIR" 2>/dev/null || return 0
    fi
    if monitor_running; then
        rmdir "$MONITOR_START_LOCK_DIR" 2>/dev/null
        return 0
    fi
    rm -f "$MONITOR_PID_FILE"
    if [ ! -x "$SCRIPT_PATH" ]; then
        SCRIPT_PATH="$0"
    fi
    nohup "$SCRIPT_PATH" monitor > /dev/null 2>&1 &
    log_msg "smartdns monitor started: $!"
    rmdir "$MONITOR_START_LOCK_DIR" 2>/dev/null
}

stop_guardian() {
    if [ -f "$GUARDIAN_PID_FILE" ]; then
        pid=$(cat "$GUARDIAN_PID_FILE" 2>/dev/null)
        if guardian_running; then
            terminate_pid "$pid"
        fi
        rm -f "$GUARDIAN_PID_FILE"
    fi
}

start_guardian() {
    if [ -f "$SYSTEMD_GUARDIAN_MARKER" ]; then
        return 0
    fi
    if ! mkdir "$GUARDIAN_START_LOCK_DIR" 2>/dev/null; then
        sleep 1
        guardian_running && return 0
        rmdir "$GUARDIAN_START_LOCK_DIR" 2>/dev/null || return 0
        mkdir "$GUARDIAN_START_LOCK_DIR" 2>/dev/null || return 0
    fi
    if guardian_running; then
        rmdir "$GUARDIAN_START_LOCK_DIR" 2>/dev/null
        return 0
    fi
    rm -f "$GUARDIAN_PID_FILE"
    if [ ! -x "$SCRIPT_PATH" ]; then
        SCRIPT_PATH="$0"
    fi
    nohup "$SCRIPT_PATH" guardian > /dev/null 2>&1 &
    log_msg "process guardian launched: $!"
    rmdir "$GUARDIAN_START_LOCK_DIR" 2>/dev/null
}

stop_daemon() {
    mkdir -p "$(dirname "$STOP_FLAG")"
    touch "$STOP_FLAG"
    rmdir "$MONITOR_START_LOCK_DIR" "$GUARDIAN_START_LOCK_DIR" 2>/dev/null
    stop_guardian
    stop_monitor
    for pid_path in "$PID_DIR"/npc_*.pid; do
        [ -f "$pid_path" ] || continue
        pid=$(cat "$pid_path" 2>/dev/null)
        if npc_pid_running "$pid" ""; then
            terminate_pid "$pid"
        fi
        rm -f "$pid_path"
    done
    if [ -f "$PID_FILE" ]; then
        rm -f "$PID_FILE"
    fi
    rm -f "${MONITOR_RUNTIME_LOCK_DIR}/pid" "${GUARDIAN_RUNTIME_LOCK_DIR}/pid" 2>/dev/null
    rmdir "$MONITOR_RUNTIME_LOCK_DIR" "$GUARDIAN_RUNTIME_LOCK_DIR" 2>/dev/null
    for proc_dir in /proc/[0-9]*; do
        [ -r "${proc_dir}/exe" ] || continue
        exe=$(readlink -f "${proc_dir}/exe" 2>/dev/null)
        [ "$exe" = "$(readlink -f "$NPC_BIN" 2>/dev/null)" ] || continue
        terminate_pid "${proc_dir##*/}"
    done
    log_msg "npc stopped"
    return 0
}

daemon_status() {
    monitor_running || return 1
    if ! guardian_running; then
        systemctl is-active --quiet nastunnel-guardian.service 2>/dev/null || return 1
    fi

    read_config
    if [ -z "$client_id" ]; then
        return 0
    fi
    if [ -f "$AUTO_START_DISABLED_MARKER" ]; then
        return 0
    fi

    found=0
    for pid_path in "$PID_DIR"/npc_*.pid; do
        [ -f "$pid_path" ] || continue
        found=1
        npc_pid_running "$(cat "$pid_path" 2>/dev/null)" "" || return 1
    done
    [ "$found" = "1" ]
}

restore_dns_if_enabled() {
    [ -f "$DNS_ENABLED_MARKER" ] || return 0
    [ -x "$DNS_HELPER" ] || return 0
    sudo -n "$DNS_HELPER" > /dev/null 2>&1 || true
}

case "$1" in
    start|connect|sync|reload-monitor|monitor|guardian)
        # SSH maintenance must use the same identity as DSM/CGI. Root-created
        # NPC/PID files are invisible and uncontrollable to the package UI.
        # Only apply to the real DSM installation, never isolated test roots.
        if [ "$(id -u)" = "0" ] &&
           [ "$INSTALL_DIR" = "/var/packages/${PACKAGE}/target" ] &&
           [ "$SCRIPT_PATH" = "/var/packages/${PACKAGE}/scripts/start-stop-status" ]; then
            id "$PACKAGE" >/dev/null 2>&1 || exit 1
            exec sudo -u "$PACKAGE" -- /bin/sh "$SCRIPT_PATH" "$@"
        fi
        ;;
esac

case "$1" in
    start)
        echo "Starting ${DNAME}..."
        rm -f "$STOP_FLAG"
        restore_dns_if_enabled
        read_config
        if [ "$auto_start" = "false" ]; then
            touch "$AUTO_START_DISABLED_MARKER"
        else
            rm -f "$AUTO_START_DISABLED_MARKER"
            sync_npc_lines
        fi
        start_monitor
        start_guardian
        exit $?
        ;;
    connect)
        echo "Connecting ${DNAME}..."
        rm -f "$STOP_FLAG" "$AUTO_START_DISABLED_MARKER"
        restore_dns_if_enabled
        sync_npc_lines
        start_monitor
        start_guardian
        exit $?
        ;;
    stop)
        echo "Stopping ${DNAME}..."
        stop_daemon
        exit $?
        ;;
    restart)
        stop_daemon
        sleep 1
        exec /bin/sh "$SCRIPT_PATH" connect
        ;;
    status)
        if daemon_status; then
            echo "${DNAME} is running"
            exit 0
        fi
        echo "${DNAME} is not running"
        exit 1
        ;;
    sync)
        sync_npc_lines
        exit $?
        ;;
    reload-monitor)
        # Reload shell functions without stopping NPC processes or changing
        # login, DNS, auto-start preferences, or the existing MUX connections.
        stop_guardian
        stop_monitor
        start_monitor
        start_guardian
        exit $?
        ;;
    monitor)
        acquire_runtime_lock "$MONITOR_RUNTIME_LOCK_DIR" || exit 0
        echo "$$" > "$MONITOR_PID_FILE"
        trap 'release_runtime_lock "$MONITOR_RUNTIME_LOCK_DIR"; log_msg "smartdns monitor loop stopped"; exit 0' TERM INT HUP
        log_msg "smartdns monitor loop started"
        while true; do
            registered_monitor=$(cat "$MONITOR_PID_FILE" 2>/dev/null)
            if [ "$registered_monitor" != "$$" ]; then
                release_runtime_lock "$MONITOR_RUNTIME_LOCK_DIR"
                exit 0
            fi
            if [ ! -f "$STOP_FLAG" ]; then
                start_guardian
            fi
            # A separate invocation picks up script hotfixes on every pass.
            "$SCRIPT_PATH" sync
            sleep "$CHECK_INTERVAL"
        done
        ;;
    guardian)
        acquire_runtime_lock "$GUARDIAN_RUNTIME_LOCK_DIR" || exit 0
        echo "$$" > "$GUARDIAN_PID_FILE"
        trap 'owner=$(cat "$GUARDIAN_PID_FILE" 2>/dev/null); [ "$owner" = "$$" ] && rm -f "$GUARDIAN_PID_FILE"; release_runtime_lock "$GUARDIAN_RUNTIME_LOCK_DIR"; log_msg "process guardian stopped"; exit 0' TERM INT HUP
        log_msg "process guardian started"
        while true; do
            owner=$(cat "$GUARDIAN_PID_FILE" 2>/dev/null)
            if [ "$owner" != "$$" ]; then
                release_runtime_lock "$GUARDIAN_RUNTIME_LOCK_DIR"
                exit 0
            fi
            if [ ! -f "$STOP_FLAG" ] && { [ -n "$NASTUNNEL_INSTALL_DIR" ] || [ -f "/var/packages/${PACKAGE}/enabled" ]; }; then
                if ! monitor_running; then
                    log_msg "monitor missing, guardian is restarting it"
                    start_monitor
                fi
            fi
            sleep "$GUARDIAN_INTERVAL"
        done
        ;;
    *)
        echo "Usage: $0 {start|connect|stop|restart|status|sync|reload-monitor|monitor|guardian}"
        exit 1
        ;;
esac
