tools/pve/add-iptag.sh
#!/usr/bin/env bash

# Copyright (c) 2021-2025 community-scripts ORG
# Author: MickLesk (Canbiz) && Desert_Gamer
# License: MIT

function header_info {
  clear
  cat <<"EOF"
 ___ ____     _____
|_ _|  _ \ _ |_   _|_ _  __ _
 | || |_) (_)  | |/ _` |/ _` |
 | ||  __/ _   | | (_| | (_| |
|___|_|   (_)  |_|\__,_|\__, |
                        |___/
EOF
}

clear
header_info
APP="IP-Tag"
hostname=$(hostname)

# Color variables
YW=$(echo "\033[33m")
GN=$(echo "\033[1;92m")
RD=$(echo "\033[01;31m")
CL=$(echo "\033[m")
BFR="\\r\\033[K"
HOLD=" "
CM=" ✔️ ${CL}"
CROSS=" ✖️ ${CL}"

# Error handler for displaying error messages
error_handler() {
  if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then
    kill $SPINNER_PID >/dev/null
  fi
  printf "\e[?25h"
  local exit_code="$?"
  local line_number="$1"
  local command="$2"
  local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
  echo -e "\n$error_message\n"
}

# Spinner for progress indication
spinner() {
  local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
  local spin_i=0
  local interval=0.1
  printf "\e[?25l"

  local color="${YWB}"

  while true; do
    printf "\r ${color}%s${CL}" "${frames[spin_i]}"
    spin_i=$(((spin_i + 1) % ${#frames[@]}))
    sleep "$interval"
  done
}

# Info message
msg_info() {
  local msg="$1"
  echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
  spinner &
  SPINNER_PID=$!
}

# Success message
msg_ok() {
  if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then
    kill $SPINNER_PID >/dev/null
  fi
  printf "\e[?25h"
  local msg="$1"
  echo -e "${BFR}${CM}${GN}${msg}${CL}"
}

# Error message
msg_error() {
  if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then
    kill $SPINNER_PID >/dev/null
  fi
  printf "\e[?25h"
  local msg="$1"
  echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}

# Check if service exists
check_service_exists() {
  if systemctl is-active --quiet iptag.service; then
    return 0
  else
    return 1
  fi
}

# Migrate configuration from old path to new
migrate_config() {
  local old_config="/opt/lxc-iptag"
  local new_config="/opt/iptag/iptag.conf"

  if [[ -f "$old_config" ]]; then
    msg_info "Migrating configuration from old path"
    if cp "$old_config" "$new_config" &>/dev/null; then
      rm -rf "$old_config" &>/dev/null
      msg_ok "Configuration migrated and old config removed"
    else
      msg_error "Failed to migrate configuration"
    fi
  fi
}

# Update existing installation
update_installation() {
  msg_info "Updating IP-Tag Scripts"
  systemctl stop iptag.service &>/dev/null
  msg_ok "Stopped IP-Tag service"

  # Create directory if it doesn't exist
  if [[ ! -d "/opt/iptag" ]]; then
    mkdir -p /opt/iptag
  fi

  # Create new config file (check if exists and ask user)
  if [[ -f "/opt/iptag/iptag.conf" ]]; then
    echo -e "\n${YW}Configuration file already exists.${CL}"
    while true; do
      read -p "Do you want to replace it with defaults? (y/n): " yn
      case $yn in
      [Yy]*)
        msg_info "Replacing configuration file"
        generate_config >/opt/iptag/iptag.conf
        msg_ok "Configuration file replaced with defaults"
        break
        ;;
      [Nn]*)
        echo -e "${GN}✔️ Keeping existing configuration file${CL}"
        break
        ;;
      *)
        echo -e "${RD}Please answer yes or no.${CL}"
        ;;
      esac
    done
  else
    msg_info "Creating new configuration file"
    generate_config >/opt/iptag/iptag.conf
    msg_ok "Created new configuration file at /opt/iptag/iptag.conf"
  fi

  # Update main script
  msg_info "Updating main script"
  generate_main_script >/opt/iptag/iptag
  chmod +x /opt/iptag/iptag
  msg_ok "Updated main script"

  # Update service file
  msg_info "Updating service file"
  generate_service >/lib/systemd/system/iptag.service
  msg_ok "Updated service file"

  msg_info "Creating manual run command"
  cat <<'EOF' >/usr/local/bin/iptag-run
#!/usr/bin/env bash
CONFIG_FILE="/opt/iptag/iptag.conf"
SCRIPT_FILE="/opt/iptag/iptag"
if [[ ! -f "$SCRIPT_FILE" ]]; then
  echo "❌ Main script not found: $SCRIPT_FILE"
  exit 1
fi
export FORCE_SINGLE_RUN=true
exec "$SCRIPT_FILE"
EOF
  chmod +x /usr/local/bin/iptag-run
  msg_ok "Created iptag-run executable - You can execute this manually by entering “iptag-run” in the Proxmox host, so the script is executed by hand."

  msg_info "Restarting service"
  systemctl daemon-reload &>/dev/null
  systemctl enable -q --now iptag.service &>/dev/null
  msg_ok "Updated IP-Tag Scripts"
}

# Generate configuration file content
generate_config() {
  cat <<EOF
# Configuration file for LXC IP tagging

# List of allowed CIDRs
CIDR_LIST=(
  192.168.0.0/16
  10.0.0.0/8
  100.64.0.0/10
)
# Enable or Disable IPv6 tagging
ENABLE_IPV6_TAGS=true

# Tag format options:
# - "full": full IP address (e.g., 192.168.0.100)
# - "last_octet": only the last octet (e.g., 100)
# - "last_two_octets": last two octets (e.g., 0.100)
TAG_FORMAT="full"

# IPv6 Tag format options:
# - "full": full IPv6 address
# - "short": compressed form (default)
# - "last_block": only last block
IPV6_TAG_FORMAT="short"

# Interval settings (in seconds) - optimized for lower CPU usage
LOOP_INTERVAL=300
VM_STATUS_CHECK_INTERVAL=600
FW_NET_INTERFACE_CHECK_INTERVAL=900
LXC_STATUS_CHECK_INTERVAL=300
FORCE_UPDATE_INTERVAL=7200

# Performance optimizations
VM_IP_CACHE_TTL=300
MAX_PARALLEL_VM_CHECKS=1

# LXC performance optimizations
LXC_IP_CACHE_TTL=300
MAX_PARALLEL_LXC_CHECKS=2

# Extreme LXC optimizations
LXC_BATCH_SIZE=3
LXC_STATUS_CACHE_TTL=300
LXC_AGGRESSIVE_CACHING=true
LXC_SKIP_SLOW_METHODS=false
LXC_ALLOW_FORCED_COMMANDS=true

# Debug settings (set to true to enable debugging)
DEBUG=false
EOF
}

# Generate systemd service file content
generate_service() {
  cat <<EOF
[Unit]
Description=IP-Tag service
After=network.target

[Service]
Type=simple
ExecStart=/opt/iptag/iptag
Restart=on-failure
RestartSec=10

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

# Generate main script content
generate_main_script() {
  cat <<'EOF'
#!/bin/bash
# =============== CONFIGURATION =============== #
readonly CONFIG_FILE="/opt/iptag/iptag.conf"
readonly DEFAULT_TAG_FORMAT="full"
readonly DEFAULT_CHECK_INTERVAL=60

# Load the configuration file if it exists
if [ -f "$CONFIG_FILE" ]; then
    # shellcheck source=./iptag.conf
    source "$CONFIG_FILE"
fi

# Set default DEBUG value if not defined
DEBUG=${DEBUG:-false}

# Debug logging function
debug_log() {
    if [[ "$DEBUG" == "true" || "$DEBUG" == "1" ]]; then
        echo "[DEBUG] $*" >&2
    fi
}

# Color constants
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[0;33m'
readonly BLUE='\033[0;34m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly WHITE='\033[1;37m'
readonly GRAY='\033[0;37m'
readonly NC='\033[0m' # No Color

# Logging functions with colors
log_success() {
    echo -e "${GREEN}✓${NC} $*"
}

log_info() {
    echo -e "${BLUE}ℹ${NC} $*"
}

log_warning() {
    echo -e "${YELLOW}⚠${NC} $*"
}

log_error() {
    echo -e "${RED}✗${NC} $*"
}

log_change() {
    echo -e "${CYAN}~${NC} $*"
}

log_unchanged() {
    echo -e "${GRAY}=${NC} $*"
}

# Check if IP is in CIDR
ip_in_cidr() {
    local ip="$1" cidr="$2"
    debug_log "ip_in_cidr: checking '$ip' against '$cidr'"

    # Manual CIDR check - более надёжный метод
    debug_log "ip_in_cidr: using manual check (bypassing ipcalc)"
        local network prefix
        IFS='/' read -r network prefix <<< "$cidr"

        # Convert IP and network to integers for comparison
        local ip_int net_int mask
        IFS='.' read -r a b c d <<< "$ip"
        ip_int=$(( (a << 24) + (b << 16) + (c << 8) + d ))

        IFS='.' read -r a b c d <<< "$network"
        net_int=$(( (a << 24) + (b << 16) + (c << 8) + d ))

    # Create subnet mask
        mask=$(( 0xFFFFFFFF << (32 - prefix) ))

    # Apply mask and compare
    local ip_masked=$((ip_int & mask))
    local net_masked=$((net_int & mask))

    debug_log "ip_in_cidr: IP=$ip ($ip_int), Network=$network ($net_int), Prefix=$prefix"
    debug_log "ip_in_cidr: Mask=$mask (hex: $(printf '0x%08x' $mask))"
    debug_log "ip_in_cidr: IP&Mask=$ip_masked ($(printf '%d.%d.%d.%d' $((ip_masked>>24&255)) $((ip_masked>>16&255)) $((ip_masked>>8&255)) $((ip_masked&255))))"
    debug_log "ip_in_cidr: Net&Mask=$net_masked ($(printf '%d.%d.%d.%d' $((net_masked>>24&255)) $((net_masked>>16&255)) $((net_masked>>8&255)) $((net_masked&255))))"

    if (( ip_masked == net_masked )); then
        debug_log "ip_in_cidr: manual check PASSED - IP is in CIDR"
        return 0
    else
        debug_log "ip_in_cidr: manual check FAILED - IP is NOT in CIDR"
        return 1
    fi
}

# Format IP address according to the configuration
format_ip_tag() {
    local ip="$1"
    [[ -z "$ip" ]] && return
    local format="${TAG_FORMAT:-$DEFAULT_TAG_FORMAT}"
    case "$format" in
        "last_octet")     echo "${ip##*.}" ;;
        "last_two_octets") echo "${ip#*.*.}" ;;
        *)               echo "$ip" ;;
    esac
}

format_ipv6_tag() {
    local ip="$1"
    [[ -z "$ip" ]] && return
    local format="${IPV6_TAG_FORMAT:-short}"

    case "$format" in
        "last_block")
            # take last hex block
            echo "${ip##*:}"
            ;;
        "full")
            # return full as-is
            echo "$ip"
            ;;
        "short"|"compressed")
            # compress repeated zeros (::) automatically
            # Linux ip command already returns compressed by default
            echo "$ip"
            ;;
        *)
            # fallback
            echo "$ip"
            ;;
    esac
}


# Check if IP is in any CIDRs
ip_in_cidrs() {
    local ip="$1" cidrs="$2"
    [[ -z "$cidrs" ]] && return 1
    local IFS=' '
    debug_log "Checking IP '$ip' against CIDRs: '$cidrs'"
    for cidr in $cidrs; do
        debug_log "Testing IP '$ip' against CIDR '$cidr'"
        if ip_in_cidr "$ip" "$cidr"; then
            debug_log "IP '$ip' matches CIDR '$cidr' - PASSED"
            return 0
        else
            debug_log "IP '$ip' does not match CIDR '$cidr'"
        fi
    done
    debug_log "IP '$ip' failed all CIDR checks"
    return 1
}

# Check if IP is valid
is_valid_ipv4() {
    local ip="$1"
    [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1

    local IFS='.' parts
    read -ra parts <<< "$ip"
    for part in "${parts[@]}"; do
        (( part >= 0 && part <= 255 )) || return 1
    done
    return 0
}

# Get VM IPs using multiple methods with performance optimizations
get_vm_ips() {
    local vmid=$1 ips=""
    local vm_config="/etc/pve/qemu-server/${vmid}.conf"
    [[ ! -f "$vm_config" ]] && return

    debug_log "vm $vmid: starting optimized IP detection"

    # Check if VM is running first (avoid expensive operations for stopped VMs)
    local vm_status=""
    if command -v qm >/dev/null 2>&1; then
        vm_status=$(qm status "$vmid" 2>/dev/null | awk '{print $2}')
    fi

    if [[ "$vm_status" != "running" ]]; then
        debug_log "vm $vmid: not running (status: $vm_status), skipping expensive detection"
        return
    fi

    # Cache for this execution
    local cache_file="/tmp/iptag_vm_${vmid}_cache"
    local cache_ttl=60  # 60 seconds cache

    # Check cache first
    if [[ -f "$cache_file" ]] && [[ $(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0))) -lt $cache_ttl ]]; then
        local cached_ips=$(cat "$cache_file" 2>/dev/null)
        if [[ -n "$cached_ips" ]]; then
            debug_log "vm $vmid: using cached IPs: $cached_ips"
            echo "$cached_ips"
            return
        fi
    fi

    # Method 1: Quick ARP table lookup (fastest)
    local mac_addresses=$(grep -E "^net[0-9]+:" "$vm_config" | grep -oE "([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}" | head -3)
    debug_log "vm $vmid: found MACs: $mac_addresses"

    # Quick ARP check without forced refresh (most common case)
    for mac in $mac_addresses; do
        local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]')
        local ip=$(ip neighbor show | grep "$mac_lower" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
        if [[ -n "$ip" && "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
            debug_log "vm $vmid: found IP $ip via quick ARP for MAC $mac_lower"
            ips+="$ip "
        fi
    done

    # Early exit if we found IPs via ARP
    if [[ -n "$ips" ]]; then
        local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ')
        unique_ips="${unique_ips% }"
        debug_log "vm $vmid: early exit with IPs: '$unique_ips'"
        echo "$unique_ips" > "$cache_file"
        echo "$unique_ips"
        return
    fi

    # Method 2: QM guest agent (fast if available)
    if command -v qm >/dev/null 2>&1; then
        local qm_ips=$(timeout 3 qm guest cmd "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v "127.0.0.1" | head -2)
        for qm_ip in $qm_ips; do
            if [[ "$qm_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
                debug_log "vm $vmid: found IP $qm_ip via qm guest cmd"
                ips+="$qm_ip "
            fi
        done
    fi

    # Early exit if we found IPs via QM
    if [[ -n "$ips" ]]; then
        local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ')
        unique_ips="${unique_ips% }"
        debug_log "vm $vmid: early exit with QM IPs: '$unique_ips'"
        echo "$unique_ips" > "$cache_file"
        echo "$unique_ips"
        return
    fi

    # Method 3: DHCP leases check (medium cost)
    for mac in $mac_addresses; do
        local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]')

        for dhcp_file in "/var/lib/dhcp/dhcpd.leases" "/var/lib/dhcpcd5/dhcpcd.leases" "/tmp/dhcp.leases"; do
            if [[ -f "$dhcp_file" ]]; then
                local dhcp_ip=$(timeout 2 grep -A 10 "ethernet $mac_lower" "$dhcp_file" 2>/dev/null | grep "binding state active" -A 5 | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" | head -1)
                if [[ -n "$dhcp_ip" && "$dhcp_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
                    debug_log "vm $vmid: found IP $dhcp_ip via DHCP leases for MAC $mac_lower"
                    ips+="$dhcp_ip "
                    break 2
                fi
            fi
        done
    done

    # Early exit if we found IPs via DHCP
    if [[ -n "$ips" ]]; then
        local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ')
        unique_ips="${unique_ips% }"
        debug_log "vm $vmid: early exit with DHCP IPs: '$unique_ips'"
        echo "$unique_ips" > "$cache_file"
        echo "$unique_ips"
        return
    fi

    # Method 4: Limited network discovery (expensive - only if really needed)
    debug_log "vm $vmid: falling back to limited network discovery"

    for mac in $mac_addresses; do
        local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]')

        # Get bridge interfaces
        local bridges=$(grep -E "^net[0-9]+:" "$vm_config" | grep -oE "bridge=\w+" | cut -d= -f2 | head -1)
        for bridge in $bridges; do
            if [[ -n "$bridge" && -d "/sys/class/net/$bridge" ]]; then
                # Get bridge IP range
                local bridge_ip=$(ip addr show "$bridge" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+' | head -1)
                if [[ -n "$bridge_ip" ]]; then
                    local network=$(echo "$bridge_ip" | cut -d'/' -f1)
                    debug_log "vm $vmid: limited scan on bridge $bridge network $bridge_ip"

                    # Force ARP refresh with broadcast ping (limited)
                    IFS='.' read -r a b c d <<< "$network"
                    local broadcast="$a.$b.$c.255"
                    timeout 1 ping -c 1 -b "$broadcast" >/dev/null 2>&1 || true

                    # Check ARP again after refresh
                    sleep 0.5
                    local ip=$(ip neighbor show | grep "$mac_lower" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
                    if [[ -n "$ip" && "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
                        debug_log "vm $vmid: found IP $ip via ARP after broadcast for MAC $mac_lower"
                        ips+="$ip "
                        break 2
                    fi

                    # Only do very limited ping scan (reduced range)
                    IFS='.' read -r a b c d <<< "$network"
                    local base_net="$a.$b.$c"

                    # Try only most common ranges (much smaller than before)
                    for last_octet in {100..105} {200..205}; do
                        local test_ip="$base_net.$last_octet"

                        # Very quick ping test (reduced timeout)
                        if timeout 0.2 ping -c 1 -W 1 "$test_ip" >/dev/null 2>&1; then
                            # Check if this IP corresponds to our MAC
                            sleep 0.1
                            local found_mac=$(ip neighbor show "$test_ip" 2>/dev/null | grep -oE "([0-9a-f]{2}:){5}[0-9a-f]{2}")
                            if [[ "$found_mac" == "$mac_lower" ]]; then
                                debug_log "vm $vmid: found IP $test_ip via limited ping scan for MAC $mac_lower"
                                ips+="$test_ip "
                                break 2
                            fi
                        fi
                    done

                    # Skip extended scanning entirely (too expensive)
                    debug_log "vm $vmid: skipping extended scan to preserve CPU"
                fi
            fi
        done
    done

    # Method 5: Static configuration check (fast)
    if [[ -z "$ips" ]]; then
        debug_log "vm $vmid: checking for static IP configuration"

        # Check cloud-init configuration if exists
        local cloudinit_file="/var/lib/vz/snippets/${vmid}-cloud-init.yml"
        if [[ -f "$cloudinit_file" ]]; then
            local static_ip=$(grep -E "addresses?:" "$cloudinit_file" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
            if [[ -n "$static_ip" && "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
                debug_log "vm $vmid: found static IP $static_ip in cloud-init config"
                ips+="$static_ip "
            fi
        fi

        # Check VM config for any IP hints
        local config_ip=$(grep -E "(ip=|gw=)" "$vm_config" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
        if [[ -n "$config_ip" && "$config_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
            debug_log "vm $vmid: found IP hint $config_ip in VM config"
            ips+="$config_ip "
        fi
    fi

    # Remove duplicates and cache result
    local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ')
    unique_ips="${unique_ips% }"

    # Cache the result (even if empty)
    echo "$unique_ips" > "$cache_file"

    debug_log "vm $vmid: final optimized IPs: '$unique_ips'"
    echo "$unique_ips"
}

# Update tags for container or VM
update_tags() {
    local type="$1" vmid="$2"
    local current_ips_full

    if [[ "$type" == "lxc" ]]; then
        current_ips_full=$(get_lxc_ips "${vmid}")
        while IFS= read -r line; do
          [[ "$line" == tags:* ]] && current_tags_raw="${line#tags: }" && break
        done < <(pct config "$vmid" 2>/dev/null)
    else
        current_ips_full=$(get_vm_ips "${vmid}")
        local vm_config="/etc/pve/qemu-server/${vmid}.conf"
        if [[ -f "$vm_config" ]]; then
            local current_tags_raw=$(grep "^tags:" "$vm_config" 2>/dev/null | cut -d: -f2 | sed 's/^[[:space:]]*//')
        fi
    fi

    local current_tags=() next_tags=() current_ip_tags=()
    if [[ -n "$current_tags_raw" ]]; then
        mapfile -t current_tags < <(echo "$current_tags_raw" | sed 's/;/\n/g')
    fi

    # Separate IP/numeric and user tags
    for tag in "${current_tags[@]}"; do
        if is_valid_ipv4 "${tag}" || [[ "$tag" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then
            current_ip_tags+=("${tag}")
        else
            next_tags+=("${tag}")
        fi
    done

    # Generate new IP tags from current IPs
    local formatted_ips=()
    debug_log "$type $vmid current_ips_full: '$current_ips_full'"
    debug_log "$type $vmid CIDR_LIST: ${CIDR_LIST[*]}"
    for ip in $current_ips_full; do
        [[ -z "$ip" ]] && continue
        debug_log "$type $vmid processing IP: '$ip'"

        if is_valid_ipv4 "$ip"; then
            debug_log "$type $vmid IP '$ip' is valid IPv4"
            # Only check IPv4 against CIDR list
            if ip_in_cidrs "$ip" "${CIDR_LIST[*]}"; then
                debug_log "$type $vmid IPv4 '$ip' passed CIDR check"
                local formatted_ip4
                formatted_ip4=$(format_ip_tag "$ip")
                debug_log "$type $vmid formatted IPv4 '$ip' -> '$formatted_ip4'"
                [[ -n "$formatted_ip4" ]] && formatted_ips+=("$formatted_ip4")
            else
                debug_log "$type $vmid IPv4 '$ip' failed CIDR check, skipping"
            fi

        elif [[ "${ENABLE_IPV6_TAGS,,}" == "true" ]]; then
            # IPv6 handling only if enabled
            debug_log "$type $vmid IP '$ip' not IPv4, treating as IPv6"
            # basic IPv6 validation
            if [[ "$ip" =~ ^[0-9a-fA-F:]+$ ]]; then
                debug_log "$type $vmid IPv6 '$ip' accepted"
                local formatted_ip6
                formatted_ip6=$(format_ipv6_tag "$ip")
                debug_log "$type $vmid formatted IPv6 '$ip' -> '$formatted_ip6'"
                [[ -n "$formatted_ip6" ]] && formatted_ips+=("$formatted_ip6")
            else
                debug_log "$type $vmid value '$ip' not recognized as valid IPv6, skipping"
            fi

        else
            debug_log "$type $vmid IP '$ip' is invalid or IPv6 not enabled"
        fi
    done
    debug_log "$type $vmid final formatted_ips: ${formatted_ips[*]}"


    # If LXC and no IPs detected, do not touch tags at all
    if [[ "$type" == "lxc" && ${#formatted_ips[@]} -eq 0 ]]; then
        log_unchanged "LXC ${GRAY}${vmid}${NC}: No IP detected, tags unchanged"
        return
    fi

    # Add new IP tags
    for new_ip in "${formatted_ips[@]}"; do
        next_tags+=("$new_ip")
    done

    # Update tags if there are changes
    local old_tags_str=$(IFS=';'; echo "${current_tags[*]}")
    local new_tags_str=$(IFS=';'; echo "${next_tags[*]}")

    debug_log "$type $vmid old_tags: '$old_tags_str'"
    debug_log "$type $vmid new_tags: '$new_tags_str'"
    debug_log "$type $vmid tags_equal: $([[ "$old_tags_str" == "$new_tags_str" ]] && echo true || echo false)"

    if [[ "$old_tags_str" != "$new_tags_str" ]]; then
        # Determine what changed
        local old_ip_tags_count=${#current_ip_tags[@]}
        local new_ip_tags_count=${#formatted_ips[@]}

        # Build detailed change message
        local change_details=""

        if [[ $old_ip_tags_count -eq 0 ]]; then
            change_details="added ${new_ip_tags_count} IP tag(s): [${GREEN}${formatted_ips[*]}${NC}]"
        else
            # Compare old and new IP tags
            local added_tags=() removed_tags=() common_tags=()

            # Find removed tags
            for old_tag in "${current_ip_tags[@]}"; do
                local found=false
                for new_tag in "${formatted_ips[@]}"; do
                    if [[ "$old_tag" == "$new_tag" ]]; then
                        found=true
                        break
                    fi
                done
                if [[ "$found" == false ]]; then
                    removed_tags+=("$old_tag")
                else
                    common_tags+=("$old_tag")
                fi
            done

            # Find added tags
            for new_tag in "${formatted_ips[@]}"; do
                local found=false
                for old_tag in "${current_ip_tags[@]}"; do
                    if [[ "$new_tag" == "$old_tag" ]]; then
                        found=true
                        break
                    fi
                done
                if [[ "$found" == false ]]; then
                    added_tags+=("$new_tag")
                fi
            done

            # Build change message
            local change_parts=()
            if [[ ${#added_tags[@]} -gt 0 ]]; then
                change_parts+=("added [${GREEN}${added_tags[*]}${NC}]")
            fi
            if [[ ${#removed_tags[@]} -gt 0 ]]; then
                change_parts+=("removed [${YELLOW}${removed_tags[*]}${NC}]")
            fi
            if [[ ${#common_tags[@]} -gt 0 ]]; then
                change_parts+=("kept [${GRAY}${common_tags[*]}${NC}]")
            fi

            change_details=$(IFS=', '; echo "${change_parts[*]}")
        fi

        log_change "${type^^} ${CYAN}${vmid}${NC}: ${change_details}"

        if [[ "$type" == "lxc" ]]; then
            pct set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null
        else
            local vm_config="/etc/pve/qemu-server/${vmid}.conf"
            if [[ -f "$vm_config" ]]; then
                sed -i '/^tags:/d' "$vm_config"
                if [[ ${#next_tags[@]} -gt 0 ]]; then
                    echo "tags: $(IFS=';'; echo "${next_tags[*]}")" >> "$vm_config"
                fi
            fi
        fi
    else
        # Tags unchanged
        local ip_count=${#formatted_ips[@]}
        local status_msg=""

        if [[ $ip_count -eq 0 ]]; then
            status_msg="No IPs detected"
        elif [[ $ip_count -eq 1 ]]; then
            status_msg="IP tag [${GRAY}${formatted_ips[0]}${NC}] unchanged"
        else
            status_msg="${ip_count} IP tags [${GRAY}${formatted_ips[*]}${NC}] unchanged"
        fi

        log_unchanged "${type^^} ${GRAY}${vmid}${NC}: ${status_msg}"
    fi
}

# Update all instances of specified type
update_all_tags() {
    local type="$1" vmids count=0

    if [[ "$type" == "lxc" ]]; then
        vmids=($(pct list 2>/dev/null | grep -v VMID | awk '{print $1}'))
    else
        local all_vm_configs=($(ls /etc/pve/qemu-server/*.conf 2>/dev/null | sed 's/.*\/\([0-9]*\)\.conf/\1/' | sort -n))
        vmids=("${all_vm_configs[@]}")
    fi

    count=${#vmids[@]}
    [[ $count -eq 0 ]] && return

    # Display processing header with color
    if [[ "$type" == "lxc" ]]; then
        log_info "Processing ${WHITE}${count}${NC} LXC container(s) in parallel"

        # Clean up old cache files before processing LXC
        cleanup_vm_cache

        # Process LXC containers in parallel for better performance
        process_lxc_parallel "${vmids[@]}"
    else
        log_info "Processing ${WHITE}${count}${NC} virtual machine(s) in parallel"

        # Clean up old cache files before processing VMs
        cleanup_vm_cache

        # Process VMs in parallel for better performance
        process_vms_parallel "${vmids[@]}"
    fi

    # Add completion message
    if [[ "$type" == "lxc" ]]; then
        log_success "Completed processing LXC containers"
    else
        log_success "Completed processing virtual machines"
    fi
}

# Check if status changed
check_status_changed() {
    local type="$1" current
    case "$type" in
        "lxc") current=$(pct list 2>/dev/null | grep -v VMID) ;;
        "vm")  current=$(ls -la /etc/pve/qemu-server/*.conf 2>/dev/null) ;;
        "fw")  current=$(ip link show type bridge 2>/dev/null) ;;
    esac
    local last_var="last_${type}_status"
    [[ "${!last_var}" == "$current" ]] && return 1
    eval "$last_var='$current'"
    return 0
}

# Main check function
check() {
    local current_time changes_detected=false
    current_time=$(date +%s)

    local update_lxc=false
    local update_vm=false

    # Periodic cache cleanup (every 10 minutes)
    local time_since_last_cleanup=$((current_time - ${last_cleanup_time:-0}))
    if [[ $time_since_last_cleanup -ge 600 ]]; then
        cleanup_vm_cache
        last_cleanup_time=$current_time
        debug_log "Performed periodic cache cleanup"
    fi

    # Check LXC status
    local time_since_last_lxc_check=$((current_time - last_lxc_status_check_time))
    if [[ "${LXC_STATUS_CHECK_INTERVAL:-60}" -gt 0 ]] && \
       [[ "$time_since_last_lxc_check" -ge "${LXC_STATUS_CHECK_INTERVAL:-60}" ]]; then
        last_lxc_status_check_time=$current_time
        if check_status_changed "lxc"; then
            update_lxc=true
            log_warning "LXC status changes detected"
        fi
    fi

    # Check VM status
    local time_since_last_vm_check=$((current_time - last_vm_status_check_time))
    if [[ "${VM_STATUS_CHECK_INTERVAL:-60}" -gt 0 ]] && \
       [[ "$time_since_last_vm_check" -ge "${VM_STATUS_CHECK_INTERVAL:-60}" ]]; then
        last_vm_status_check_time=$current_time
        if check_status_changed "vm"; then
            update_vm=true
            log_warning "VM status changes detected"
        fi
    fi

    # Check network interface changes
    local time_since_last_fw_check=$((current_time - last_fw_net_interface_check_time))
    if [[ "${FW_NET_INTERFACE_CHECK_INTERVAL:-60}" -gt 0 ]] && \
       [[ "$time_since_last_fw_check" -ge "${FW_NET_INTERFACE_CHECK_INTERVAL:-60}" ]]; then
        last_fw_net_interface_check_time=$current_time
        if check_status_changed "fw"; then
            update_lxc=true
            update_vm=true
            log_warning "Network interface changes detected"
        fi
    fi

    # Force update if interval exceeded
    for type in "lxc" "vm"; do
        local last_update_var="last_update_${type}_time"
        local time_since_last_update=$((current_time - ${!last_update_var}))
        if [[ $time_since_last_update -ge ${FORCE_UPDATE_INTERVAL:-1800} ]]; then
            if [[ "$type" == "lxc" ]]; then
                update_lxc=true
                log_info "Scheduled LXC update (every $((FORCE_UPDATE_INTERVAL / 60)) minutes)"
            else
                update_vm=true
                log_info "Scheduled VM update (every $((FORCE_UPDATE_INTERVAL / 60)) minutes)"
            fi
            eval "${last_update_var}=${current_time}"
        fi
    done

    # Final execution
    $update_lxc && update_all_tags "lxc"
    $update_vm && update_all_tags "vm"
}

# Initialize time variables
declare -g last_lxc_status="" last_vm_status="" last_fw_status=""
declare -g last_lxc_status_check_time=0 last_vm_status_check_time=0 last_fw_net_interface_check_time=0
declare -g last_update_lxc_time=0 last_update_vm_time=0 last_cleanup_time=0

# Main loop
main() {
    # Display startup message
    echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
    log_success "IP-Tag service started successfully"
    echo -e "${BLUE}ℹ${NC} Loop interval: ${WHITE}${LOOP_INTERVAL:-$DEFAULT_CHECK_INTERVAL}${NC} seconds"
    echo -e "${BLUE}ℹ${NC} Debug mode: ${WHITE}${DEBUG:-false}${NC}"
    echo -e "${BLUE}ℹ${NC} Tag format: ${WHITE}${TAG_FORMAT:-$DEFAULT_TAG_FORMAT}${NC}"
    echo -e "${BLUE}ℹ${NC} Allowed CIDRs: ${WHITE}${CIDR_LIST[*]}${NC}"
    echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"

    if [[ "$FORCE_SINGLE_RUN" == "true" ]]; then
        check
        exit 0
    fi

    while true; do
        check
        sleep "${LOOP_INTERVAL:-300}"
    done
}


# Cache cleanup function
cleanup_vm_cache() {
    local cache_dir="/tmp"
    local vm_cache_ttl=${VM_IP_CACHE_TTL:-120}
    local lxc_cache_ttl=${LXC_IP_CACHE_TTL:-120}
    local status_cache_ttl=${LXC_STATUS_CACHE_TTL:-30}
    local current_time=$(date +%s)

    debug_log "Starting extreme cache cleanup"

    # Clean VM cache files
    for cache_file in "$cache_dir"/iptag_vm_*_cache; do
        if [[ -f "$cache_file" ]]; then
            local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
            if [[ $((current_time - file_time)) -gt $vm_cache_ttl ]]; then
                rm -f "$cache_file" 2>/dev/null
                debug_log "Cleaned up expired VM cache file: $cache_file"
            fi
        fi
    done

    # Clean LXC IP cache files
    for cache_file in "$cache_dir"/iptag_lxc_*_cache; do
        if [[ -f "$cache_file" ]]; then
            local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
            if [[ $((current_time - file_time)) -gt $lxc_cache_ttl ]]; then
                rm -f "$cache_file" 2>/dev/null
                # Also clean meta files
                rm -f "${cache_file}.meta" 2>/dev/null
                debug_log "Cleaned up expired LXC cache file: $cache_file"
            fi
        fi
    done

    # Clean LXC status cache files (shorter TTL)
    for cache_file in "$cache_dir"/iptag_lxc_status_*_cache; do
        if [[ -f "$cache_file" ]]; then
            local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
            if [[ $((current_time - file_time)) -gt $status_cache_ttl ]]; then
                rm -f "$cache_file" 2>/dev/null
                debug_log "Cleaned up expired LXC status cache: $cache_file"
            fi
        fi
    done

    # Clean LXC PID cache files (60 second TTL)
    for cache_file in "$cache_dir"/iptag_lxc_pid_*_cache; do
        if [[ -f "$cache_file" ]]; then
            local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
            if [[ $((current_time - file_time)) -gt 60 ]]; then
                rm -f "$cache_file" 2>/dev/null
                debug_log "Cleaned up expired LXC PID cache: $cache_file"
            fi
        fi
    done

    # Clean any orphaned meta files
    for meta_file in "$cache_dir"/iptag_*.meta; do
        if [[ -f "$meta_file" ]]; then
            local base_file="${meta_file%.meta}"
            if [[ ! -f "$base_file" ]]; then
                rm -f "$meta_file" 2>/dev/null
                debug_log "Cleaned up orphaned meta file: $meta_file"
            fi
        fi
    done

    debug_log "Completed extreme cache cleanup"
}

# Parallel VM processing function
process_vms_parallel() {
    local vm_list=("$@")
    local max_parallel=${MAX_PARALLEL_VM_CHECKS:-5}
    local job_count=0
    local pids=()
    local pid_start_times=()

    for vmid in "${vm_list[@]}"; do
        if [[ $job_count -ge $max_parallel ]]; then
            local pid_to_wait="${pids[0]}"
            local start_time="${pid_start_times[0]}"
            local waited=0
            while kill -0 "$pid_to_wait" 2>/dev/null && [[ $waited -lt 10 ]]; do
                sleep 1
                ((waited++))
            done
            if kill -0 "$pid_to_wait" 2>/dev/null; then
                kill -9 "$pid_to_wait" 2>/dev/null
                log_warning "VM parallel: killed stuck process $pid_to_wait after 10s timeout"
            else
                wait "$pid_to_wait"
            fi
            pids=("${pids[@]:1}")
            pid_start_times=("${pid_start_times[@]:1}")
            ((job_count--))
        fi
        # Start background job
        (update_tags "vm" "$vmid") &
        pids+=($!)
        pid_start_times+=("$(date +%s)")
        ((job_count++))
    done
    for i in "${!pids[@]}"; do
        local pid="${pids[$i]}"
        local waited=0
        while kill -0 "$pid" 2>/dev/null && [[ $waited -lt 10 ]]; do
            sleep 1
            ((waited++))
        done
        if kill -0 "$pid" 2>/dev/null; then
            kill -9 "$pid" 2>/dev/null
            log_warning "VM parallel: killed stuck process $pid after 10s timeout"
        else
            wait "$pid"
        fi
    done
}

# Parallel LXC processing function
process_lxc_parallel() {
    local lxc_list=("$@")
    local max_parallel=${MAX_PARALLEL_LXC_CHECKS:-2}
    local batch_size=${LXC_BATCH_SIZE:-20}
    local job_count=0
    local pids=()
    local pid_start_times=()

    debug_log "Starting parallel LXC processing: ${#lxc_list[@]} containers, max_parallel=$max_parallel"

    if [[ ${#lxc_list[@]} -gt 5 ]]; then
        debug_log "Pre-loading LXC statuses for ${#lxc_list[@]} containers"
        local all_statuses=$(pct list 2>/dev/null)
        for vmid in "${lxc_list[@]}"; do
            local status=$(echo "$all_statuses" | grep "^$vmid" | awk '{print $2}')
            if [[ -n "$status" ]]; then
                local status_cache_file="/tmp/iptag_lxc_status_${vmid}_cache"
                echo "$status" > "$status_cache_file" 2>/dev/null &
            fi
        done
        wait
        debug_log "Completed batch status pre-loading"
    fi
    for vmid in "${lxc_list[@]}"; do
        if [[ $job_count -ge $max_parallel ]]; then
            local pid_to_wait="${pids[0]}"
            local start_time="${pid_start_times[0]}"
            local waited=0
            while kill -0 "$pid_to_wait" 2>/dev/null && [[ $waited -lt 10 ]]; do
                sleep 1
                ((waited++))
            done
            if kill -0 "$pid_to_wait" 2>/dev/null; then
                kill -9 "$pid_to_wait" 2>/dev/null
                log_warning "LXC parallel: killed stuck process $pid_to_wait after 10s timeout"
            else
                wait "$pid_to_wait"
            fi
            pids=("${pids[@]:1}")
            pid_start_times=("${pid_start_times[@]:1}")
            ((job_count--))
        fi
        # Start background job with higher priority
        (update_tags "lxc" "$vmid") &
        pids+=($!)
        pid_start_times+=("$(date +%s)")
        ((job_count++))
    done
    for i in "${!pids[@]}"; do
        local pid="${pids[$i]}"
        local waited=0
        while kill -0 "$pid" 2>/dev/null && [[ $waited -lt 10 ]]; do
            sleep 1
            ((waited++))
        done
        if kill -0 "$pid" 2>/dev/null; then
            kill -9 "$pid" 2>/dev/null
            log_warning "LXC parallel: killed stuck process $pid after 10s timeout"
        else
            wait "$pid"
        fi
    done
    debug_log "Completed parallel LXC processing"
}

# Optimized LXC IP detection with caching and alternative methods
# -------------------------------------------
# Combined optimized LXC IP detection
# Keeps advanced debug logs & methods
# Adds IPv6 detection controlled by ENABLE_IPV6_TAGS
# -------------------------------------------
get_lxc_ips() {
    local vmid=$1
    local ips=""
    local method_used=""

    # status cache for container state
    local status_cache_file="/tmp/iptag_lxc_status_${vmid}_cache"
    local status_cache_ttl=${LXC_STATUS_CACHE_TTL:-30}

    debug_log "lxc $vmid: starting combined IP detection"

    # ----- STATUS CHECK -----
    local lxc_status=""
    if [[ -f "$status_cache_file" ]] && [[ $(($(date +%s) - $(stat -c %Y "$status_cache_file" 2>/dev/null || echo 0))) -lt $status_cache_ttl ]]; then
        lxc_status=$(cat "$status_cache_file" 2>/dev/null)
        debug_log "lxc $vmid: using cached status: $lxc_status"
    else
        lxc_status=$(pct status "${vmid}" 2>/dev/null | awk '{print $2}')
        echo "$lxc_status" > "$status_cache_file" 2>/dev/null
        debug_log "lxc $vmid: fetched fresh status: $lxc_status"
    fi

    if [[ "$lxc_status" != "running" ]]; then
        debug_log "lxc $vmid: not running (status: $lxc_status)"
        return
    fi

    # ----- TRY CONFIG FOR STATIC IP -----
    local pve_lxc_config="/etc/pve/lxc/${vmid}.conf"
    if [[ -f "$pve_lxc_config" ]]; then
        local static_ip=$(grep -E "^net[0-9]+:" "$pve_lxc_config" 2>/dev/null | grep -oE 'ip=([0-9]{1,3}\.){3}[0-9]{1,3}' | cut -d'=' -f2 | head -1)
        debug_log "lxc $vmid: [CONFIG] static_ip='$static_ip'"
        if [[ -n "$static_ip" && "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
            ips="$static_ip"
            method_used="proxmox_config"
        fi
    fi

    # ----- NAMESPACE FAST PARSE -----
    if [[ -z "$ips" ]]; then
        local ns_file="/var/lib/lxc/${vmid}/rootfs/proc/net/fib_trie"
        debug_log "lxc $vmid: trying namespace fib_trie"
        if [[ -f "$ns_file" ]]; then
            local ns_ip=$(timeout 1 grep -m1 -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' "$ns_file" 2>/dev/null | grep -v '127.0.0.1' | head -1)
            if [[ -n "$ns_ip" ]] && is_valid_ipv4 "$ns_ip"; then
                ips="$ns_ip"
                method_used="namespace_fib"
                debug_log "lxc $vmid: found IP via namespace: $ips"
            fi
        fi
    fi

    # ----- ARP TABLE -----
    if [[ -z "$ips" ]]; then
        debug_log "lxc $vmid: trying ARP lookup"
        local mac_addr=$(grep -Eo 'hwaddr=([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "$pve_lxc_config" | head -1 | cut -d'=' -f2 | tr 'A-F' 'a-f')
        if [[ -n "$mac_addr" ]]; then
            local bridge_ip=$(ip neighbor show | grep "$mac_addr" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
            if [[ -n "$bridge_ip" && "$bridge_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
                ips="$bridge_ip"
                method_used="arp_table"
                debug_log "lxc $vmid: found IP via ARP: $ips"
            fi
        fi
    fi

    # ----- PROCESS NAMESPACE (fast) -----
    if [[ -z "$ips" && "${LXC_SKIP_SLOW_METHODS:-true}" != "true" ]]; then
        debug_log "lxc $vmid: trying process namespace"
        local pid_cache_file="/tmp/iptag_lxc_pid_${vmid}_cache"
        local container_pid=""
        if [[ -f "$pid_cache_file" ]] && [[ $(($(date +%s) - $(stat -c %Y "$pid_cache_file" 2>/dev/null || echo 0))) -lt 60 ]]; then
            container_pid=$(cat "$pid_cache_file" 2>/dev/null)
        else
            container_pid=$(pct list 2>/dev/null | grep "^$vmid" | awk '{print $3}')
            [[ -n "$container_pid" && "$container_pid" != "-" ]] && echo "$container_pid" > "$pid_cache_file"
        fi
        if [[ -n "$container_pid" && "$container_pid" != "-" ]]; then
            local ns_ip=$(timeout 1 nsenter -t "$container_pid" -n ip -4 addr show 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1)
            if [[ -n "$ns_ip" ]] && is_valid_ipv4 "$ns_ip"; then
                ips="$ns_ip"
                method_used="process_ns"
                debug_log "lxc $vmid: found IP via process namespace: $ips"
            fi
        fi
    fi

    # ----- FORCED METHODS (attach/exec) -----
    if [[ -z "$ips" && "${LXC_ALLOW_FORCED_COMMANDS:-false}" == "true" ]]; then
        debug_log "lxc $vmid: trying forced pct exec"
        local pct_ip=$(timeout 7s pct exec "$vmid" -- ip -4 addr show 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1)
        if [[ -n "$pct_ip" ]] && is_valid_ipv4 "$pct_ip"; then
            ips="$pct_ip"
            method_used="pct_exec_forced"
            debug_log "lxc $vmid: found IP via pct exec: $ips"
        fi
    fi

    # ----- OPTIONAL IPv6 detection -----
    if [[ "${ENABLE_IPV6_TAGS,,}" == "true" ]]; then
        debug_log "lxc $vmid: IPv6 detection enabled"

        # 1. Try to get IPv6 from inside the container
        local pct_ipv6=$(timeout 3 pct exec "$vmid" -- ip -6 addr show scope global 2>/dev/null \
                         | grep -oE '([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}' \
                         | grep -v '^fe80' | head -1)
        if [[ -n "$pct_ipv6" ]]; then
            debug_log "lxc $vmid: found IPv6 via pct exec: $pct_ipv6"
            [[ -n "$ips" ]] && ips="$ips $pct_ipv6" || ips="$pct_ipv6"
            method_used="${method_used:+$method_used,}ipv6_exec"
        else
            debug_log "lxc $vmid: no IPv6 from pct exec, fallback to neighbor table"

            # 2. Fallback: neighbor table
            local mac_addr=$(grep -Eo 'hwaddr=([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "$pve_lxc_config" \
                              | head -1 | cut -d'=' -f2 | tr 'A-F' 'a-f')
            if [[ -n "$mac_addr" ]]; then
                local ipv6_nb=$(ip -6 neighbor show | grep -i "$mac_addr" \
                                 | grep -oE '([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}' \
                                 | grep -v '^fe80' | head -1)
                if [[ -n "$ipv6_nb" ]]; then
                    debug_log "lxc $vmid: found IPv6 via neighbor: $ipv6_nb"
                    [[ -n "$ips" ]] && ips="$ips $ipv6_nb" || ips="$ipv6_nb"
                    method_used="${method_used:+$method_used,}ipv6_neighbor"
                else
                    debug_log "lxc $vmid: no IPv6 found in neighbor table"
                fi
            fi
        fi
    fi

    # ----- FINAL RESULT -----
    debug_log "lxc $vmid: [RESULT] ips='$ips' method='$method_used'"
    echo "$ips"
}


main
EOF
}

# Main installation process
if check_service_exists; then
  while true; do
    read -p "IP-Tag service is already installed. Do you want to update it? (y/n): " yn
    case $yn in
    [Yy]*)
      update_installation
      exit 0
      ;;
    [Nn]*)
      msg_error "Installation cancelled."
      exit 0
      ;;
    *)
      msg_error "Please answer yes or no."
      ;;
    esac
  done
fi

while true; do
  read -p "This will install ${APP} on ${hostname}. Proceed? (y/n): " yn
  case $yn in
  [Yy]*)
    break
    ;;
  [Nn]*)
    msg_error "Installation cancelled."
    exit
    ;;
  *)
    msg_error "Please answer yes or no."
    ;;
  esac
done

if ! pveversion | grep -Eq "pve-manager/8\.[0-4](\.[0-9]+)*"; then
  msg_error "This version of Proxmox Virtual Environment is not supported"
  msg_error "⚠️ Requires Proxmox Virtual Environment Version 8.0 or later."
  msg_error "Exiting..."
  sleep 2
  exit
fi

FILE_PATH="/usr/local/bin/iptag"
if [[ -f "$FILE_PATH" ]]; then
  msg_info "The file already exists: '$FILE_PATH'. Skipping installation."
  exit 0
fi

msg_info "Installing Dependencies"
apt-get update &>/dev/null
apt-get install -y ipcalc net-tools &>/dev/null
msg_ok "Installed Dependencies"

msg_info "Setting up IP-Tag Scripts"
mkdir -p /opt/iptag
msg_ok "Setup IP-Tag Scripts"

# Migrate config if needed
migrate_config

msg_info "Setup Default Config"
if [[ ! -f /opt/iptag/iptag.conf ]]; then
  generate_config >/opt/iptag/iptag.conf
  msg_ok "Setup default config"
else
  msg_ok "Default config already exists"
fi

msg_info "Setup Main Function"
if [[ ! -f /opt/iptag/iptag ]]; then
  generate_main_script >/opt/iptag/iptag
  chmod +x /opt/iptag/iptag
  msg_ok "Setup Main Function"
else
  msg_ok "Main Function already exists"
fi

msg_info "Creating Service"
if [[ ! -f /lib/systemd/system/iptag.service ]]; then
  generate_service >/lib/systemd/system/iptag.service
  msg_ok "Created Service"
else
  msg_ok "Service already exists."
fi

msg_ok "Setup IP-Tag Scripts"

msg_info "Starting Service"
systemctl daemon-reload &>/dev/null
systemctl enable -q --now iptag.service &>/dev/null
msg_ok "Started Service"

msg_info "Restarting Service with optimizations"
systemctl restart iptag.service &>/dev/null
msg_ok "Service restarted with CPU optimizations"

msg_info "Creating manual run command"
cat <<'EOF' >/usr/local/bin/iptag-run
#!/usr/bin/env bash
CONFIG_FILE="/opt/iptag/iptag.conf"
SCRIPT_FILE="/opt/iptag/iptag"
if [[ ! -f "$SCRIPT_FILE" ]]; then
  echo "❌ Main script not found: $SCRIPT_FILE"
  exit 1
fi
export FORCE_SINGLE_RUN=true
exec "$SCRIPT_FILE"
EOF
chmod +x /usr/local/bin/iptag-run
msg_ok "Created iptag-run executable - You can execute this manually by entering “iptag-run” in the Proxmox host, so the script is executed by hand."

SPINNER_PID=""
echo -e "\n${APP} installation completed successfully! ${CL}\n"

# Proper script termination
exit 0
