#!/bin/bash

# ==============================================================================
# HELPER FUNCTIONS FOR PACKAGE MANAGEMENT
# ==============================================================================

# ------------------------------------------------------------------------------
# Cache installed version to avoid repeated checks
# ------------------------------------------------------------------------------
cache_installed_version() {
  local app="$1"
  local version="$2"
  mkdir -p /var/cache/app-versions
  echo "$version" >"/var/cache/app-versions/${app}_version.txt"
}

get_cached_version() {
  local app="$1"
  mkdir -p /var/cache/app-versions
  if [[ -f "/var/cache/app-versions/${app}_version.txt" ]]; then
    cat "/var/cache/app-versions/${app}_version.txt"
    return 0
  fi
  return 0
}

# ------------------------------------------------------------------------------
# Unified package upgrade function (with apt update caching)
# ------------------------------------------------------------------------------
upgrade_package() {
  local package="$1"

  # Use same caching logic as ensure_dependencies
  local apt_cache_file="/var/cache/apt-update-timestamp"
  local current_time=$(date +%s)
  local last_update=0

  if [[ -f "$apt_cache_file" ]]; then
    last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
  fi

  if ((current_time - last_update > 300)); then
    $STD apt update
    echo "$current_time" >"$apt_cache_file"
  fi

  $STD apt install --only-upgrade -y "$package"
}

# ------------------------------------------------------------------------------
# Repository availability check
# ------------------------------------------------------------------------------
verify_repo_available() {
  local repo_url="$1"
  local suite="$2"

  if curl -fsSL --max-time 10 "${repo_url}/dists/${suite}/Release" &>/dev/null; then
    return 0
  fi
  return 1
}

# ------------------------------------------------------------------------------
# Ensure dependencies are installed (with apt update caching)
# ------------------------------------------------------------------------------
ensure_dependencies() {
  local deps=("$@")
  local missing=()

  for dep in "${deps[@]}"; do
    if ! command -v "$dep" &>/dev/null && ! is_package_installed "$dep"; then
      missing+=("$dep")
    fi
  done

  if [[ ${#missing[@]} -gt 0 ]]; then
    # Only run apt update if not done recently (within last 5 minutes)
    local apt_cache_file="/var/cache/apt-update-timestamp"
    local current_time=$(date +%s)
    local last_update=0

    if [[ -f "$apt_cache_file" ]]; then
      last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
    fi

    if ((current_time - last_update > 300)); then
      # Ensure orphaned sources are cleaned before updating
      cleanup_orphaned_sources 2>/dev/null || true

      if ! $STD apt update; then
        ensure_apt_working || return 1
      fi
      echo "$current_time" >"$apt_cache_file"
    fi

    $STD apt install -y "${missing[@]}" || {
      msg_error "Failed to install dependencies: ${missing[*]}"
      return 1
    }
  fi
}

# ------------------------------------------------------------------------------
# Smart version comparison
# ------------------------------------------------------------------------------
version_gt() {
  test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
}

# ------------------------------------------------------------------------------
# Get system architecture (normalized)
# ------------------------------------------------------------------------------
get_system_arch() {
  local arch_type="${1:-dpkg}" # dpkg, uname, or both
  local arch

  case "$arch_type" in
  dpkg)
    arch=$(dpkg --print-architecture 2>/dev/null)
    ;;
  uname)
    arch=$(uname -m)
    [[ "$arch" == "x86_64" ]] && arch="amd64"
    [[ "$arch" == "aarch64" ]] && arch="arm64"
    ;;
  both | *)
    arch=$(dpkg --print-architecture 2>/dev/null || uname -m)
    [[ "$arch" == "x86_64" ]] && arch="amd64"
    [[ "$arch" == "aarch64" ]] && arch="arm64"
    ;;
  esac

  echo "$arch"
}

# ------------------------------------------------------------------------------
# Create temporary directory with automatic cleanup
# ------------------------------------------------------------------------------
create_temp_dir() {
  local tmp_dir=$(mktemp -d)
  # Set trap to cleanup on EXIT, ERR, INT, TERM
  trap "rm -rf '$tmp_dir'" EXIT ERR INT TERM
  echo "$tmp_dir"
}

# ------------------------------------------------------------------------------
# Check if package is installed (faster than dpkg -l | grep)
# ------------------------------------------------------------------------------
is_package_installed() {
  local package="$1"
  dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "^install ok installed$"
}

# ------------------------------------------------------------------------------
# GitHub API call with authentication and rate limit handling
# ------------------------------------------------------------------------------
github_api_call() {
  local url="$1"
  local output_file="${2:-/dev/stdout}"
  local max_retries=3
  local retry_delay=2

  local header_args=()
  [[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN")

  for attempt in $(seq 1 $max_retries); do
    local http_code
    http_code=$(curl -fsSL -w "%{http_code}" -o "$output_file" \
      -H "Accept: application/vnd.github+json" \
      -H "X-GitHub-Api-Version: 2022-11-28" \
      "${header_args[@]}" \
      "$url" 2>/dev/null || echo "000")

    case "$http_code" in
    200)
      return 0
      ;;
    403)
      # Rate limit - check if we can retry
      if [[ $attempt -lt $max_retries ]]; then
        msg_warn "GitHub API rate limit, waiting ${retry_delay}s... (attempt $attempt/$max_retries)"
        sleep "$retry_delay"
        retry_delay=$((retry_delay * 2))
        continue
      fi
      msg_error "GitHub API rate limit exceeded. Set GITHUB_TOKEN to increase limits."
      return 1
      ;;
    404)
      msg_error "GitHub API endpoint not found: $url"
      return 1
      ;;
    *)
      if [[ $attempt -lt $max_retries ]]; then
        sleep "$retry_delay"
        continue
      fi
      msg_error "GitHub API call failed with HTTP $http_code"
      return 1
      ;;
    esac
  done

  return 1
}

should_upgrade() {
  local current="$1"
  local target="$2"

  [[ -z "$current" ]] && return 0
  version_gt "$target" "$current" && return 0
  return 1
}

# ------------------------------------------------------------------------------
# Get OS information (cached for performance)
# ------------------------------------------------------------------------------
get_os_info() {
  local field="${1:-all}" # id, codename, version, version_id, all

  # Cache OS info to avoid repeated file reads
  if [[ -z "${_OS_ID:-}" ]]; then
    export _OS_ID=$(awk -F= '/^ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
    export _OS_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
    export _OS_VERSION=$(awk -F= '/^VERSION_ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
    export _OS_VERSION_FULL=$(awk -F= '/^VERSION=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
  fi

  case "$field" in
  id) echo "$_OS_ID" ;;
  codename) echo "$_OS_CODENAME" ;;
  version) echo "$_OS_VERSION" ;;
  version_id) echo "$_OS_VERSION" ;;
  version_full) echo "$_OS_VERSION_FULL" ;;
  all) echo "ID=$_OS_ID CODENAME=$_OS_CODENAME VERSION=$_OS_VERSION" ;;
  *) echo "$_OS_ID" ;;
  esac
}

# ------------------------------------------------------------------------------
# Check if running on specific OS
# ------------------------------------------------------------------------------
is_debian() {
  [[ "$(get_os_info id)" == "debian" ]]
}

is_ubuntu() {
  [[ "$(get_os_info id)" == "ubuntu" ]]
}

is_alpine() {
  [[ "$(get_os_info id)" == "alpine" ]]
}

# ------------------------------------------------------------------------------
# Get Debian/Ubuntu major version
# ------------------------------------------------------------------------------
get_os_version_major() {
  local version=$(get_os_info version)
  echo "${version%%.*}"
}

# ------------------------------------------------------------------------------
# Download file with retry logic and progress
# ------------------------------------------------------------------------------
download_file() {
  local url="$1"
  local output="$2"
  local max_retries="${3:-3}"
  local show_progress="${4:-false}"

  local curl_opts=(-fsSL)
  [[ "$show_progress" == "true" ]] && curl_opts=(-fL#)

  for attempt in $(seq 1 $max_retries); do
    if curl "${curl_opts[@]}" -o "$output" "$url"; then
      return 0
    fi

    if [[ $attempt -lt $max_retries ]]; then
      msg_warn "Download failed, retrying... (attempt $attempt/$max_retries)"
      sleep 2
    fi
  done

  msg_error "Failed to download: $url"
  return 1
}

# ------------------------------------------------------------------------------
# Get fallback suite for repository (comprehensive mapping)
# ------------------------------------------------------------------------------
get_fallback_suite() {
  local distro_id="$1"
  local distro_codename="$2"
  local repo_base_url="$3"

  # Check if current codename works
  if verify_repo_available "$repo_base_url" "$distro_codename"; then
    echo "$distro_codename"
    return 0
  fi

  # Comprehensive fallback mappings
  case "$distro_id" in
  debian)
    case "$distro_codename" in
    # Debian 13 (Trixie) → Debian 12 (Bookworm)
    trixie | forky | sid)
      echo "bookworm"
      ;;
    # Debian 12 (Bookworm) stays
    bookworm)
      echo "bookworm"
      ;;
    # Debian 11 (Bullseye) stays
    bullseye)
      echo "bullseye"
      ;;
    # Unknown → latest stable
    *)
      echo "bookworm"
      ;;
    esac
    ;;
  ubuntu)
    case "$distro_codename" in
    # Ubuntu 24.10 (Oracular) → 24.04 LTS (Noble)
    oracular | plucky)
      echo "noble"
      ;;
    # Ubuntu 24.04 LTS (Noble) stays
    noble)
      echo "noble"
      ;;
    # Ubuntu 23.10 (Mantic) → 22.04 LTS (Jammy)
    mantic | lunar)
      echo "jammy"
      ;;
    # Ubuntu 22.04 LTS (Jammy) stays
    jammy)
      echo "jammy"
      ;;
    # Ubuntu 20.04 LTS (Focal) stays
    focal)
      echo "focal"
      ;;
    # Unknown → latest LTS
    *)
      echo "jammy"
      ;;
    esac
    ;;
  *)
    echo "$distro_codename"
    ;;
  esac
}

# ------------------------------------------------------------------------------
# Verify package source and version
# ------------------------------------------------------------------------------
verify_package_source() {
  local package="$1"
  local expected_version="$2"

  if apt-cache policy "$package" 2>/dev/null | grep -q "$expected_version"; then
    return 0
  fi
  return 1
}

# ------------------------------------------------------------------------------
# Check if running on LTS version
# ------------------------------------------------------------------------------
is_lts_version() {
  local os_id=$(get_os_info id)
  local codename=$(get_os_info codename)

  if [[ "$os_id" == "ubuntu" ]]; then
    case "$codename" in
    focal | jammy | noble) return 0 ;; # 20.04, 22.04, 24.04
    *) return 1 ;;
    esac
  elif [[ "$os_id" == "debian" ]]; then
    # Debian releases are all "stable"
    case "$codename" in
    bullseye | bookworm | trixie) return 0 ;;
    *) return 1 ;;
    esac
  fi

  return 1
}

# ------------------------------------------------------------------------------
# Get optimal number of parallel jobs (cached)
# ------------------------------------------------------------------------------
get_parallel_jobs() {
  if [[ -z "${_PARALLEL_JOBS:-}" ]]; then
    local cpu_count=$(nproc 2>/dev/null || echo 1)
    local mem_gb=$(free -g | awk '/^Mem:/{print $2}')

    # Limit by available memory (assume 1GB per job for compilation)
    local max_by_mem=$((mem_gb > 0 ? mem_gb : 1))
    local max_jobs=$((cpu_count < max_by_mem ? cpu_count : max_by_mem))

    # At least 1, at most cpu_count
    export _PARALLEL_JOBS=$((max_jobs > 0 ? max_jobs : 1))
  fi
  echo "$_PARALLEL_JOBS"
}

# ------------------------------------------------------------------------------
# Get default PHP version for OS
# ------------------------------------------------------------------------------
get_default_php_version() {
  local os_id=$(get_os_info id)
  local os_version=$(get_os_version_major)

  case "$os_id" in
  debian)
    case "$os_version" in
    13) echo "8.3" ;; # Debian 13 (Trixie)
    12) echo "8.2" ;; # Debian 12 (Bookworm)
    11) echo "7.4" ;; # Debian 11 (Bullseye)
    *) echo "8.2" ;;
    esac
    ;;
  ubuntu)
    case "$os_version" in
    24) echo "8.3" ;; # Ubuntu 24.04 LTS (Noble)
    22) echo "8.1" ;; # Ubuntu 22.04 LTS (Jammy)
    20) echo "7.4" ;; # Ubuntu 20.04 LTS (Focal)
    *) echo "8.1" ;;
    esac
    ;;
  *)
    echo "8.2"
    ;;
  esac
}

# ------------------------------------------------------------------------------
# Get default Python version for OS
# ------------------------------------------------------------------------------
get_default_python_version() {
  local os_id=$(get_os_info id)
  local os_version=$(get_os_version_major)

  case "$os_id" in
  debian)
    case "$os_version" in
    13) echo "3.12" ;; # Debian 13 (Trixie)
    12) echo "3.11" ;; # Debian 12 (Bookworm)
    11) echo "3.9" ;;  # Debian 11 (Bullseye)
    *) echo "3.11" ;;
    esac
    ;;
  ubuntu)
    case "$os_version" in
    24) echo "3.12" ;; # Ubuntu 24.04 LTS
    22) echo "3.10" ;; # Ubuntu 22.04 LTS
    20) echo "3.8" ;;  # Ubuntu 20.04 LTS
    *) echo "3.10" ;;
    esac
    ;;
  *)
    echo "3.11"
    ;;
  esac
}

# ------------------------------------------------------------------------------
# Get default Node.js LTS version
# ------------------------------------------------------------------------------
get_default_nodejs_version() {
  # Always return current LTS (as of 2025)
  echo "20"
}

# ------------------------------------------------------------------------------
# Check if package manager is locked
# ------------------------------------------------------------------------------
is_apt_locked() {
  if fuser /var/lib/dpkg/lock-frontend &>/dev/null ||
    fuser /var/lib/apt/lists/lock &>/dev/null ||
    fuser /var/cache/apt/archives/lock &>/dev/null; then
    return 0
  fi
  return 1
}

# ------------------------------------------------------------------------------
# Wait for apt to be available
# ------------------------------------------------------------------------------
wait_for_apt() {
  local max_wait="${1:-300}" # 5 minutes default
  local waited=0

  while is_apt_locked; do
    if [[ $waited -ge $max_wait ]]; then
      msg_error "Timeout waiting for apt to be available"
      return 1
    fi

    sleep 5
    waited=$((waited + 5))
  done

  return 0
}

# ------------------------------------------------------------------------------
# Cleanup old repository files (migration helper)
# ------------------------------------------------------------------------------
cleanup_old_repo_files() {
  local app="$1"

  # Remove old-style .list files (including backups)
  rm -f /etc/apt/sources.list.d/"${app}"*.list
  rm -f /etc/apt/sources.list.d/"${app}"*.list.save
  rm -f /etc/apt/sources.list.d/"${app}"*.list.distUpgrade
  rm -f /etc/apt/sources.list.d/"${app}"*.list.dpkg-*

  # Remove old GPG keys from trusted.gpg.d
  rm -f /etc/apt/trusted.gpg.d/"${app}"*.gpg

  # Remove keyrings from /etc/apt/keyrings
  rm -f /etc/apt/keyrings/"${app}"*.gpg

  # Remove ALL .sources files for this app (including the main one)
  # This ensures no orphaned .sources files reference deleted keyrings
  rm -f /etc/apt/sources.list.d/"${app}"*.sources
}

# ------------------------------------------------------------------------------
# Cleanup orphaned .sources files that reference missing keyrings
# This prevents APT signature verification errors
# Call this at the start of any setup function to ensure APT is in a clean state
# ------------------------------------------------------------------------------
cleanup_orphaned_sources() {
  local sources_dir="/etc/apt/sources.list.d"
  local keyrings_dir="/etc/apt/keyrings"

  [[ ! -d "$sources_dir" ]] && return 0

  while IFS= read -r -d '' sources_file; do
    local basename_file
    basename_file=$(basename "$sources_file")

    # NEVER remove debian.sources - this is the standard Debian repository
    if [[ "$basename_file" == "debian.sources" ]]; then
      continue
    fi

    # Extract Signed-By path from .sources file
    local keyring_path
    keyring_path=$(grep -E '^Signed-By:' "$sources_file" 2>/dev/null | awk '{print $2}')

    # If keyring doesn't exist, remove the .sources file
    if [[ -n "$keyring_path" ]] && [[ ! -f "$keyring_path" ]]; then
      rm -f "$sources_file"
    fi
  done < <(find "$sources_dir" -name "*.sources" -print0 2>/dev/null)

  # Also check for broken symlinks in keyrings directory
  if [[ -d "$keyrings_dir" ]]; then
    find "$keyrings_dir" -type l ! -exec test -e {} \; -delete 2>/dev/null || true
  fi
}

# ------------------------------------------------------------------------------
# Ensure APT is in a working state before installing packages
# This should be called at the start of any setup function
# ------------------------------------------------------------------------------
ensure_apt_working() {
  # Clean up orphaned sources first
  cleanup_orphaned_sources

  # Try to update package lists
  if ! apt-get update -qq 2>/dev/null; then
    # More aggressive cleanup
    rm -f /etc/apt/sources.list.d/*.sources 2>/dev/null || true
    cleanup_orphaned_sources

    # Try again
    if ! apt-get update -qq 2>/dev/null; then
      msg_error "Cannot update package lists - APT is critically broken"
      return 1
    fi
  fi

  return 0
}

# ------------------------------------------------------------------------------
# Standardized deb822 repository setup
# ------------------------------------------------------------------------------
setup_deb822_repo() {
  local name="$1"
  local gpg_url="$2"
  local repo_url="$3"
  local suite="$4"
  local component="${5:-main}"
  local architectures="${6:-amd64 arm64}"

  # Cleanup old configs for this app
  cleanup_old_repo_files "$name"

  # Cleanup any orphaned .sources files from other apps
  cleanup_orphaned_sources

  # Ensure keyring directory exists
  mkdir -p /etc/apt/keyrings

  # Download GPG key (with --yes to avoid interactive prompts)
  curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" || {
    msg_error "Failed to download or import GPG key for ${name}"
    return 1
  }

  # Create deb822 sources file
  cat <<EOF >/etc/apt/sources.list.d/${name}.sources
Types: deb
URIs: $repo_url
Suites: $suite
Components: $component
Architectures: $architectures
Signed-By: /etc/apt/keyrings/${name}.gpg
EOF

  # Use cached apt update
  local apt_cache_file="/var/cache/apt-update-timestamp"
  local current_time=$(date +%s)
  local last_update=0

  if [[ -f "$apt_cache_file" ]]; then
    last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
  fi

  # For repo changes, always update but respect short-term cache (30s)
  if ((current_time - last_update > 30)); then
    $STD apt update
    echo "$current_time" >"$apt_cache_file"
  fi
}

# ------------------------------------------------------------------------------
# Package version hold/unhold helpers
# ------------------------------------------------------------------------------
hold_package_version() {
  local package="$1"
  $STD apt-mark hold "$package"
}

unhold_package_version() {
  local package="$1"
  $STD apt-mark unhold "$package"
}

# ------------------------------------------------------------------------------
# Safe service restart with verification
# ------------------------------------------------------------------------------
safe_service_restart() {
  local service="$1"

  if systemctl is-active --quiet "$service"; then
    $STD systemctl restart "$service"
  else
    $STD systemctl start "$service"
  fi

  if ! systemctl is-active --quiet "$service"; then
    msg_error "Failed to start $service"
    systemctl status "$service" --no-pager
    return 1
  fi
  return 0
}

# ------------------------------------------------------------------------------
# Enable and start service (with error handling)
# ------------------------------------------------------------------------------
enable_and_start_service() {
  local service="$1"

  if ! systemctl enable "$service" &>/dev/null; then
    return 1
  fi

  if ! systemctl start "$service" &>/dev/null; then
    msg_error "Failed to start $service"
    systemctl status "$service" --no-pager
    return 1
  fi

  return 0
}

# ------------------------------------------------------------------------------
# Check if service is enabled
# ------------------------------------------------------------------------------
is_service_enabled() {
  local service="$1"
  systemctl is-enabled --quiet "$service" 2>/dev/null
}

# ------------------------------------------------------------------------------
# Check if service is running
# ------------------------------------------------------------------------------
is_service_running() {
  local service="$1"
  systemctl is-active --quiet "$service" 2>/dev/null
}

# ------------------------------------------------------------------------------
# Extract version from JSON (GitHub releases)
# ------------------------------------------------------------------------------
extract_version_from_json() {
  local json="$1"
  local field="${2:-tag_name}"
  local strip_v="${3:-true}"

  ensure_dependencies jq

  local version
  version=$(echo "$json" | jq -r ".${field} // empty")

  if [[ -z "$version" ]]; then
    return 1
  fi

  if [[ "$strip_v" == "true" ]]; then
    echo "${version#v}"
  else
    echo "$version"
  fi
}

# ------------------------------------------------------------------------------
# Get latest GitHub release version
# ------------------------------------------------------------------------------
get_latest_github_release() {
  local repo="$1"
  local strip_v="${2:-true}"
  local temp_file=$(mktemp)

  if ! github_api_call "https://api.github.com/repos/${repo}/releases/latest" "$temp_file"; then
    rm -f "$temp_file"
    return 1
  fi

  local version
  version=$(extract_version_from_json "$(cat "$temp_file")" "tag_name" "$strip_v")
  rm -f "$temp_file"

  if [[ -z "$version" ]]; then
    return 1
  fi

  echo "$version"
}

# ------------------------------------------------------------------------------
# Debug logging (only if DEBUG=1)
# ------------------------------------------------------------------------------
debug_log() {
  [[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2
}

# ------------------------------------------------------------------------------
# Performance timing helper
# ------------------------------------------------------------------------------
start_timer() {
  echo $(date +%s)
}

end_timer() {
  local start_time="$1"
  local label="${2:-Operation}"
  local end_time=$(date +%s)
  local duration=$((end_time - start_time))
}

# ------------------------------------------------------------------------------
# GPG key fingerprint verification
# ------------------------------------------------------------------------------
verify_gpg_fingerprint() {
  local key_file="$1"
  local expected_fingerprint="$2"

  local actual_fingerprint
  actual_fingerprint=$(gpg --show-keys --with-fingerprint --with-colons "$key_file" 2>&1 | grep -m1 '^fpr:' | cut -d: -f10)

  if [[ "$actual_fingerprint" == "$expected_fingerprint" ]]; then
    return 0
  fi

  msg_error "GPG fingerprint mismatch! Expected: $expected_fingerprint, Got: $actual_fingerprint"
  return 1
}

# ==============================================================================
# EXISTING FUNCTIONS
# ==============================================================================

# ------------------------------------------------------------------------------
# Checks for new GitHub release (latest tag).
#
# Description:
#   - Queries the GitHub API for the latest release tag
#   - Compares it to a local cached version (~/.<app>)
#   - If newer, sets global CHECK_UPDATE_RELEASE and returns 0
#
# Usage:
#     if check_for_gh_release "flaresolverr" "FlareSolverr/FlareSolverr" [optional] "v1.1.1"; then
#       # trigger update...
#     fi
#     exit 0
#     } (end of update_script not from the function)
#
# Notes:
#   - Requires `jq` (auto-installed if missing)
#   - Does not modify anything, only checks version state
#   - Does not support pre-releases
# ------------------------------------------------------------------------------
check_for_gh_release() {
  local app="$1"
  local source="$2"
  local pinned_version_in="${3:-}" # optional
  local app_lc="${app,,}"
  local current_file="$HOME/.${app_lc}"

  msg_info "Checking for update: ${app}"

  # DNS check
  if ! getent hosts api.github.com >/dev/null 2>&1; then
    msg_error "Network error: cannot resolve api.github.com"
    return 1
  fi

  ensure_dependencies jq

  # Fetch releases and exclude drafts/prereleases
  local releases_json
  releases_json=$(curl -fsSL --max-time 20 \
    -H 'Accept: application/vnd.github+json' \
    -H 'X-GitHub-Api-Version: 2022-11-28' \
    "https://api.github.com/repos/${source}/releases") || {
    msg_error "Unable to fetch releases for ${app}"
    return 1
  }

  mapfile -t raw_tags < <(jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<<"$releases_json")
  if ((${#raw_tags[@]} == 0)); then
    msg_error "No stable releases found for ${app}"
    return 1
  fi

  local clean_tags=()
  for t in "${raw_tags[@]}"; do
    clean_tags+=("${t#v}")
  done

  local latest_raw="${raw_tags[0]}"
  local latest_clean="${clean_tags[0]}"

  # current installed (stored without v)
  local current=""
  if [[ -f "$current_file" ]]; then
    current="$(<"$current_file")"
  else
    # Migration: search for any /opt/*_version.txt
    local legacy_files
    mapfile -t legacy_files < <(find /opt -maxdepth 1 -type f -name "*_version.txt" 2>/dev/null)
    if ((${#legacy_files[@]} == 1)); then
      current="$(<"${legacy_files[0]}")"
      echo "${current#v}" >"$current_file"
      rm -f "${legacy_files[0]}"
    fi
  fi
  current="${current#v}"

  # Pinned version handling
  if [[ -n "$pinned_version_in" ]]; then
    local pin_clean="${pinned_version_in#v}"
    local match_raw=""
    for i in "${!clean_tags[@]}"; do
      if [[ "${clean_tags[$i]}" == "$pin_clean" ]]; then
        match_raw="${raw_tags[$i]}"
        break
      fi
    done

    if [[ -z "$match_raw" ]]; then
      msg_error "Pinned version ${pinned_version_in} not found upstream"
      return 1
    fi

    if [[ "$current" != "$pin_clean" ]]; then
      CHECK_UPDATE_RELEASE="$match_raw"
      msg_ok "Checking for update: ${app}"
      return 0
    fi

    msg_ok "Checking for update: ${app}"
    return 1
  fi

  # No pinning → use latest
  if [[ -z "$current" || "$current" != "$latest_clean" ]]; then
    CHECK_UPDATE_RELEASE="$latest_raw"
    msg_ok "Checking for update: ${app}"
    return 0
  fi

  msg_ok "Checking for update: ${app}"
  return 1
}

# ------------------------------------------------------------------------------
# Creates and installs self-signed certificates.
#
# Description:
#   - Create a self-signed certificate with option to override application name
#
# Variables:
#   APP   - Application name (default: $APPLICATION variable)
# ------------------------------------------------------------------------------
create_self_signed_cert() {
  local APP_NAME="${1:-${APPLICATION}}"
  local CERT_DIR="/etc/ssl/${APP_NAME}"
  local CERT_KEY="${CERT_DIR}/${APP_NAME}.key"
  local CERT_CRT="${CERT_DIR}/${APP_NAME}.crt"

  if [[ -f "$CERT_CRT" && -f "$CERT_KEY" ]]; then
    return 0
  fi

  $STD apt update || {
    msg_error "Failed to update package list"
    return 1
  }
  $STD apt install -y openssl || {
    msg_error "Failed to install OpenSSL"
    return 1
  }

  mkdir -p "$CERT_DIR"
  $STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
    -subj "/C=US/ST=State/L=City/O=Organization/CN=${APP_NAME}" \
    -keyout "$CERT_KEY" \
    -out "$CERT_CRT" || {
    msg_error "Failed to create self-signed certificate"
    return 1
  }

  chmod 600 "$CERT_KEY"
  chmod 644 "$CERT_CRT"
}

# ------------------------------------------------------------------------------
# Downloads file with optional progress indicator using pv.
#
# Arguments:
#   $1 - URL
#   $2 - Destination path
# ------------------------------------------------------------------------------

function download_with_progress() {
  local url="$1"
  local output="$2"
  if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi

  ensure_dependencies pv
  set -o pipefail

  # Content-Length aus HTTP-Header holen
  local content_length
  content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true)

  if [[ -z "$content_length" ]]; then
    if ! curl -fL# -o "$output" "$url"; then
      msg_error "Download failed"
      return 1
    fi
  else
    if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then
      msg_error "Download failed"
      return 1
    fi
  fi
}

# ------------------------------------------------------------------------------
# Ensures /usr/local/bin is permanently in system PATH.
#
# Description:
#   - Adds to /etc/profile.d if not present
# ------------------------------------------------------------------------------

function ensure_usr_local_bin_persist() {
  local PROFILE_FILE="/etc/profile.d/custom_path.sh"

  if [[ ! -f "$PROFILE_FILE" ]] && ! command -v pveversion &>/dev/null; then
    echo 'export PATH="/usr/local/bin:$PATH"' >"$PROFILE_FILE"
    chmod +x "$PROFILE_FILE"
  fi
}

# ------------------------------------------------------------------------------
# Downloads and deploys latest GitHub release (source, binary, tarball, asset).
#
# Description:
#   - Fetches latest release metadata from GitHub API
#   - Supports the following modes:
#       - tarball: Source code tarball (default if omitted)
#       - source: Alias for tarball (same behavior)
#       - binary: .deb package install (arch-dependent)
#       - prebuild: Prebuilt .tar.gz archive (e.g. Go binaries)
#       - singlefile: Standalone binary (no archive, direct chmod +x install)
#   - Handles download, extraction/installation and version tracking in ~/.<app>
#
# Parameters:
#   $1   APP               - Application name (used for install path and version file)
#   $2   REPO              - GitHub repository in form user/repo
#   $3   MODE              - Release type:
#                              tarball   → source tarball (.tar.gz)
#                              binary    → .deb file (auto-arch matched)
#                              prebuild  → prebuilt archive (e.g. tar.gz)
#                              singlefile→ standalone binary (chmod +x)
#   $4   VERSION           - Optional release tag (default: latest)
#   $5   TARGET_DIR        - Optional install path (default: /opt/<app>)
#   $6   ASSET_FILENAME    - Required for:
#                              - prebuild  → archive filename or pattern
#                              - singlefile→ binary filename or pattern
#
# Optional:
#   - Set GITHUB_TOKEN env var to increase API rate limit (recommended for CI/CD).
#
# Examples:
#   # 1. Minimal: Fetch and deploy source tarball
#   fetch_and_deploy_gh_release "myapp" "myuser/myapp"
#
#   # 2. Binary install via .deb asset (architecture auto-detected)
#   fetch_and_deploy_gh_release "myapp" "myuser/myapp" "binary"
#
#   # 3. Prebuilt archive (.tar.gz) with asset filename match
#   fetch_and_deploy_gh_release "hanko" "teamhanko/hanko" "prebuild" "latest" "/opt/hanko" "hanko_Linux_x86_64.tar.gz"
#
#   # 4. Single binary (chmod +x) like Argus, Promtail etc.
#   fetch_and_deploy_gh_release "argus" "release-argus/Argus" "singlefile" "0.26.3" "/opt/argus" "Argus-.*linux-amd64"
# ------------------------------------------------------------------------------

function fetch_and_deploy_gh_release() {
  local app="$1"
  local repo="$2"
  local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile
  local version="${4:-latest}"
  local target="${5:-/opt/$app}"
  local asset_pattern="${6:-}"

  local app_lc=$(echo "${app,,}" | tr -d ' ')
  local version_file="$HOME/.${app_lc}"

  local api_timeout="--connect-timeout 10 --max-time 60"
  local download_timeout="--connect-timeout 15 --max-time 900"

  local current_version=""
  [[ -f "$version_file" ]] && current_version=$(<"$version_file")

  ensure_dependencies jq

  local api_url="https://api.github.com/repos/$repo/releases"
  [[ "$version" != "latest" ]] && api_url="$api_url/tags/$version" || api_url="$api_url/latest"
  local header=()
  [[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN")

  # dns pre check
  local gh_host
  gh_host=$(awk -F/ '{print $3}' <<<"$api_url")
  if ! getent hosts "$gh_host" &>/dev/null; then
    msg_error "DNS resolution failed for $gh_host – check /etc/resolv.conf or networking"
    return 1
  fi

  local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code

  while ((attempt <= max_retries)); do
    resp=$(curl $api_timeout -fsSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url") && success=true && break
    sleep "$retry_delay"
    ((attempt++))
  done

  if ! $success; then
    msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts"
    return 1
  fi

  http_code="${resp:(-3)}"
  [[ "$http_code" != "200" ]] && {
    msg_error "GitHub API returned HTTP $http_code"
    return 1
  }

  local json tag_name
  json=$(</tmp/gh_rel.json)
  tag_name=$(echo "$json" | jq -r '.tag_name // .name // empty')
  [[ "$tag_name" =~ ^v ]] && version="${tag_name:1}" || version="$tag_name"

  if [[ "$current_version" == "$version" ]]; then
    $STD msg_ok "$app is already up-to-date (v$version)"
    return 0
  fi

  local tmpdir
  tmpdir=$(mktemp -d) || return 1
  local filename="" url=""

  msg_info "Fetching GitHub release: $app ($version)"

  local clean_install=false
  [[ -n "${CLEAN_INSTALL:-}" && "$CLEAN_INSTALL" == "1" ]] && clean_install=true

  ### Tarball Mode ###
  if [[ "$mode" == "tarball" || "$mode" == "source" ]]; then
    url=$(echo "$json" | jq -r '.tarball_url // empty')
    [[ -z "$url" ]] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz"
    filename="${app_lc}-${version}.tar.gz"

    curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url" || {
      msg_error "Download failed: $url"
      rm -rf "$tmpdir"
      return 1
    }

    mkdir -p "$target"
    if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
      rm -rf "${target:?}/"*
    fi

    tar -xzf "$tmpdir/$filename" -C "$tmpdir" || {
      msg_error "Failed to extract tarball"
      rm -rf "$tmpdir"
      return 1
    }
    local unpack_dir
    unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1)

    shopt -s dotglob nullglob
    cp -r "$unpack_dir"/* "$target/"
    shopt -u dotglob nullglob

    ### Binary Mode ###
  elif [[ "$mode" == "binary" ]]; then
    local arch
    arch=$(dpkg --print-architecture 2>/dev/null || uname -m)
    [[ "$arch" == "x86_64" ]] && arch="amd64"
    [[ "$arch" == "aarch64" ]] && arch="arm64"

    local assets url_match=""
    assets=$(echo "$json" | jq -r '.assets[].browser_download_url')

    # If explicit filename pattern is provided (param $6), match that first
    if [[ -n "$asset_pattern" ]]; then
      for u in $assets; do
        case "${u##*/}" in
        $asset_pattern)
          url_match="$u"
          break
          ;;
        esac
      done
    fi

    # If no match via explicit pattern, fall back to architecture heuristic
    if [[ -z "$url_match" ]]; then
      for u in $assets; do
        if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then
          url_match="$u"
          break
        fi
      done
    fi

    # Fallback: any .deb file
    if [[ -z "$url_match" ]]; then
      for u in $assets; do
        [[ "$u" =~ \.deb$ ]] && url_match="$u" && break
      done
    fi

    if [[ -z "$url_match" ]]; then
      msg_error "No suitable .deb asset found for $app"
      rm -rf "$tmpdir"
      return 1
    fi

    filename="${url_match##*/}"
    curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url_match" || {
      msg_error "Download failed: $url_match"
      rm -rf "$tmpdir"
      return 1
    }

    chmod 644 "$tmpdir/$filename"
    $STD apt install -y "$tmpdir/$filename" || {
      $STD dpkg -i "$tmpdir/$filename" || {
        msg_error "Both apt and dpkg installation failed"
        rm -rf "$tmpdir"
        return 1
      }
    }

    ### Prebuild Mode ###
  elif [[ "$mode" == "prebuild" ]]; then
    local pattern="${6%\"}"
    pattern="${pattern#\"}"
    [[ -z "$pattern" ]] && {
      msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)"
      rm -rf "$tmpdir"
      return 1
    }

    local asset_url=""
    for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do
      filename_candidate="${u##*/}"
      case "$filename_candidate" in
      $pattern)
        asset_url="$u"
        break
        ;;
      esac
    done

    [[ -z "$asset_url" ]] && {
      msg_error "No asset matching '$pattern' found"
      rm -rf "$tmpdir"
      return 1
    }

    filename="${asset_url##*/}"
    curl $download_timeout -fsSL -o "$tmpdir/$filename" "$asset_url" || {
      msg_error "Download failed: $asset_url"
      rm -rf "$tmpdir"
      return 1
    }

    local unpack_tmp
    unpack_tmp=$(mktemp -d)
    mkdir -p "$target"
    if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
      rm -rf "${target:?}/"*
    fi

    if [[ "$filename" == *.zip ]]; then
      ensure_dependencies unzip
      unzip -q "$tmpdir/$filename" -d "$unpack_tmp" || {
        msg_error "Failed to extract ZIP archive"
        rm -rf "$tmpdir" "$unpack_tmp"
        return 1
      }
    elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then
      tar -xf "$tmpdir/$filename" -C "$unpack_tmp" || {
        msg_error "Failed to extract TAR archive"
        rm -rf "$tmpdir" "$unpack_tmp"
        return 1
      }
    else
      msg_error "Unsupported archive format: $filename"
      rm -rf "$tmpdir" "$unpack_tmp"
      return 1
    fi

    local top_dirs
    top_dirs=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1 -type d | wc -l)
    local top_entries inner_dir
    top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1)
    if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then
      # Strip leading folder
      inner_dir="$top_entries"
      shopt -s dotglob nullglob
      if compgen -G "$inner_dir/*" >/dev/null; then
        cp -r "$inner_dir"/* "$target/" || {
          msg_error "Failed to copy contents from $inner_dir to $target"
          rm -rf "$tmpdir" "$unpack_tmp"
          return 1
        }
      else
        msg_error "Inner directory is empty: $inner_dir"
        rm -rf "$tmpdir" "$unpack_tmp"
        return 1
      fi
      shopt -u dotglob nullglob
    else
      # Copy all contents
      shopt -s dotglob nullglob
      if compgen -G "$unpack_tmp/*" >/dev/null; then
        cp -r "$unpack_tmp"/* "$target/" || {
          msg_error "Failed to copy contents to $target"
          rm -rf "$tmpdir" "$unpack_tmp"
          return 1
        }
      else
        msg_error "Unpacked archive is empty"
        rm -rf "$tmpdir" "$unpack_tmp"
        return 1
      fi
      shopt -u dotglob nullglob
    fi

    ### Singlefile Mode ###
  elif [[ "$mode" == "singlefile" ]]; then
    local pattern="${6%\"}"
    pattern="${pattern#\"}"
    [[ -z "$pattern" ]] && {
      msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)"
      rm -rf "$tmpdir"
      return 1
    }

    local asset_url=""
    for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do
      filename_candidate="${u##*/}"
      case "$filename_candidate" in
      $pattern)
        asset_url="$u"
        break
        ;;
      esac
    done

    [[ -z "$asset_url" ]] && {
      msg_error "No asset matching '$pattern' found"
      rm -rf "$tmpdir"
      return 1
    }

    filename="${asset_url##*/}"
    mkdir -p "$target"

    local use_filename="${USE_ORIGINAL_FILENAME:-false}"
    local target_file="$app"
    [[ "$use_filename" == "true" ]] && target_file="$filename"

    curl $download_timeout -fsSL -o "$target/$target_file" "$asset_url" || {
      msg_error "Download failed: $asset_url"
      rm -rf "$tmpdir"
      return 1
    }

    if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then
      chmod +x "$target/$target_file"
    fi

  else
    msg_error "Unknown mode: $mode"
    rm -rf "$tmpdir"
    return 1
  fi

  echo "$version" >"$version_file"
  msg_ok "Deployed: $app ($version)"
  rm -rf "$tmpdir"
}

# ------------------------------------------------------------------------------
# Loads LOCAL_IP from persistent store or detects if missing.
#
# Description:
#   - Loads from /run/local-ip.env or performs runtime lookup
# ------------------------------------------------------------------------------

function import_local_ip() {
  local IP_FILE="/run/local-ip.env"
  if [[ -f "$IP_FILE" ]]; then
    # shellcheck disable=SC1090
    source "$IP_FILE"
  fi

  if [[ -z "${LOCAL_IP:-}" ]]; then
    get_current_ip() {
      local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default")
      local ip

      for target in "${targets[@]}"; do
        if [[ "$target" == "default" ]]; then
          ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
        else
          ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
        fi
        if [[ -n "$ip" ]]; then
          echo "$ip"
          return 0
        fi
      done

      return 1
    }

    LOCAL_IP="$(get_current_ip || true)"
    if [[ -z "$LOCAL_IP" ]]; then
      msg_error "Could not determine LOCAL_IP"
      return 1
    fi
  fi

  export LOCAL_IP
}

# ------------------------------------------------------------------------------
# Installs Adminer (Debian/Ubuntu via APT, Alpine via direct download).
#
# Description:
#   - Adds Adminer to Apache or web root
#   - Supports Alpine and Debian-based systems
# ------------------------------------------------------------------------------

function setup_adminer() {
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "adminer")

  if grep -qi alpine /etc/os-release; then
    msg_info "Setup Adminer (Alpine)"
    mkdir -p /var/www/localhost/htdocs/adminer
    curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \
      -o /var/www/localhost/htdocs/adminer/index.php || {
      msg_error "Failed to download Adminer"
      return 1
    }
    cache_installed_version "adminer" "latest-alpine"
    msg_ok "Setup Adminer (Alpine)"
  else
    msg_info "Setup Adminer (Debian/Ubuntu)"
    ensure_dependencies adminer
    $STD a2enconf adminer || {
      msg_error "Failed to enable Adminer Apache config"
      return 1
    }
    $STD systemctl reload apache2 || {
      msg_error "Failed to reload Apache"
      return 1
    }
    local VERSION
    VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}')
    cache_installed_version "adminer" "${VERSION:-unknown}"
    msg_ok "Setup Adminer (Debian/Ubuntu)"
  fi
}

# ------------------------------------------------------------------------------
# Installs or updates Composer globally (robust, idempotent).
#
# - Installs to /usr/local/bin/composer
# - Removes old binaries/symlinks in /usr/bin, /bin, /root/.composer, etc.
# - Ensures /usr/local/bin is in PATH (permanent)
# ------------------------------------------------------------------------------

function setup_composer() {
  local COMPOSER_BIN="/usr/local/bin/composer"
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "composer")
  export COMPOSER_ALLOW_SUPERUSER=1

  msg_info "Setup Composer"

  for old in /usr/bin/composer /bin/composer /root/.composer/vendor/bin/composer; do
    [[ -e "$old" && "$old" != "$COMPOSER_BIN" ]] && rm -f "$old"
  done

  ensure_usr_local_bin_persist
  export PATH="/usr/local/bin:$PATH"

  curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
    msg_error "Failed to download Composer installer"
    return 1
  }

  $STD php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer || {
    msg_error "Failed to install Composer"
    rm -f /tmp/composer-setup.php
    return 1
  }
  rm -f /tmp/composer-setup.php

  if [[ ! -x "$COMPOSER_BIN" ]]; then
    msg_error "Composer installation failed"
    return 1
  fi

  chmod +x "$COMPOSER_BIN"
  $STD "$COMPOSER_BIN" self-update --no-interaction || true

  local FINAL_VERSION
  FINAL_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
  cache_installed_version "composer" "$FINAL_VERSION"
  msg_ok "Setup Composer"
}

# ------------------------------------------------------------------------------
# Installs FFmpeg from source or prebuilt binary (Debian/Ubuntu only).
#
# Description:
#   - Downloads and builds FFmpeg from GitHub (https://github.com/FFmpeg/FFmpeg)
#   - Supports specific version override via FFMPEG_VERSION (e.g. n7.1.1)
#   - Supports build profile via FFMPEG_TYPE:
#       - minimal : x264, vpx, mp3 only
#       - medium  : adds subtitles, fonts, opus, vorbis
#       - full    : adds dav1d, svt-av1, zlib, numa
#       - binary  : downloads static build (johnvansickle.com)
#   - Defaults to latest stable version and full feature set
#
# Notes:
#   - Requires: curl, jq, build-essential, and matching codec libraries
#   - Result is installed to /usr/local/bin/ffmpeg
# ------------------------------------------------------------------------------

function setup_ffmpeg() {
  local TMP_DIR=$(mktemp -d)
  local GITHUB_REPO="FFmpeg/FFmpeg"
  local VERSION="${FFMPEG_VERSION:-latest}"
  local TYPE="${FFMPEG_TYPE:-full}"
  local BIN_PATH="/usr/local/bin/ffmpeg"
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "ffmpeg")

  msg_info "Setup FFmpeg ${VERSION} ($TYPE)"

  # Binary fallback mode
  if [[ "$TYPE" == "binary" ]]; then
    curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o "$TMP_DIR/ffmpeg.tar.xz" || {
      msg_error "Failed to download FFmpeg binary"
      rm -rf "$TMP_DIR"
      return 1
    }
    tar -xf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" || {
      msg_error "Failed to extract FFmpeg binary"
      rm -rf "$TMP_DIR"
      return 1
    }
    local EXTRACTED_DIR
    EXTRACTED_DIR=$(find "$TMP_DIR" -maxdepth 1 -type d -name "ffmpeg-*")
    cp "$EXTRACTED_DIR/ffmpeg" "$BIN_PATH"
    cp "$EXTRACTED_DIR/ffprobe" /usr/local/bin/ffprobe
    chmod +x "$BIN_PATH" /usr/local/bin/ffprobe
    local FINAL_VERSION=$($BIN_PATH -version | head -n1 | awk '{print $3}')
    rm -rf "$TMP_DIR"
    cache_installed_version "ffmpeg" "$FINAL_VERSION"
    ensure_usr_local_bin_persist
    msg_ok "Setup FFmpeg"
    return 0
  fi

  ensure_dependencies jq

  # Auto-detect latest stable version if none specified
  if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
    VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/tags" |
      jq -r '.[].name' |
      grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' |
      sort -V | tail -n1)
  fi

  if [[ -z "$VERSION" ]]; then
    msg_error "Could not determine FFmpeg version"
    rm -rf "$TMP_DIR"
    return 1
  fi

  # Dependency selection
  local DEPS=(build-essential yasm nasm pkg-config)
  case "$TYPE" in
  minimal)
    DEPS+=(libx264-dev libvpx-dev libmp3lame-dev)
    ;;
  medium)
    DEPS+=(libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev)
    ;;
  full)
    DEPS+=(
      libx264-dev libx265-dev libvpx-dev libmp3lame-dev
      libfreetype6-dev libass-dev libopus-dev libvorbis-dev
      libdav1d-dev libsvtav1-dev zlib1g-dev libnuma-dev
      libva-dev libdrm-dev
    )
    ;;
  *)
    msg_error "Invalid FFMPEG_TYPE: $TYPE"
    rm -rf "$TMP_DIR"
    return 1
    ;;
  esac

  ensure_dependencies "${DEPS[@]}"

  curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" || {
    msg_error "Failed to download FFmpeg source"
    rm -rf "$TMP_DIR"
    return 1
  }

  tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || {
    msg_error "Failed to extract FFmpeg"
    rm -rf "$TMP_DIR"
    return 1
  }

  cd "$TMP_DIR/FFmpeg-"* || {
    msg_error "Source extraction failed"
    rm -rf "$TMP_DIR"
    return 1
  }

  local args=(
    --enable-gpl
    --enable-shared
    --enable-nonfree
    --disable-static
    --enable-libx264
    --enable-libvpx
    --enable-libmp3lame
  )

  if [[ "$TYPE" != "minimal" ]]; then
    args+=(--enable-libfreetype --enable-libass --enable-libopus --enable-libvorbis)
  fi

  if [[ "$TYPE" == "full" ]]; then
    args+=(--enable-libx265 --enable-libdav1d --enable-zlib)
    args+=(--enable-vaapi --enable-libdrm)
  fi

  if [[ ${#args[@]} -eq 0 ]]; then
    msg_error "FFmpeg configure args array is empty"
    rm -rf "$TMP_DIR"
    return 1
  fi

  $STD ./configure "${args[@]}" || {
    msg_error "FFmpeg configure failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  $STD make -j"$(nproc)" || {
    msg_error "FFmpeg compilation failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  $STD make install || {
    msg_error "FFmpeg installation failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf
  $STD ldconfig

  ldconfig -p | grep libavdevice >/dev/null || {
    msg_error "libavdevice not registered with dynamic linker"
    rm -rf "$TMP_DIR"
    return 1
  }

  if ! command -v ffmpeg &>/dev/null; then
    msg_error "FFmpeg installation failed"
    rm -rf "$TMP_DIR"
    return 1
  fi

  local FINAL_VERSION
  FINAL_VERSION=$(ffmpeg -version | head -n1 | awk '{print $3}')
  rm -rf "$TMP_DIR"
  cache_installed_version "ffmpeg" "$FINAL_VERSION"
  ensure_usr_local_bin_persist
  msg_ok "Setup FFmpeg"
}

# ------------------------------------------------------------------------------
# Installs Go (Golang) from official tarball.
#
# Description:
#   - Determines system architecture
#   - Downloads latest version if GO_VERSION not set
#
# Variables:
#   GO_VERSION     - Version to install (e.g. 1.22.2 or latest)
# ------------------------------------------------------------------------------

function setup_go() {
  local ARCH
  case "$(uname -m)" in
  x86_64) ARCH="amd64" ;;
  aarch64) ARCH="arm64" ;;
  *)
    msg_error "Unsupported architecture: $(uname -m)"
    return 1
    ;;
  esac

  # Determine version
  if [[ -z "${GO_VERSION:-}" || "${GO_VERSION}" == "latest" ]]; then
    GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text | head -n1 | sed 's/^go//')
    if [[ -z "$GO_VERSION" ]]; then
      msg_error "Could not determine latest Go version"
      return 1
    fi
  fi

  local GO_BIN="/usr/local/bin/go"
  local GO_INSTALL_DIR="/usr/local/go"
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "go")

  if [[ -x "$GO_BIN" ]]; then
    local CURRENT_VERSION
    CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//')
    if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then
      if [[ "$CACHED_VERSION" == "$GO_VERSION" ]]; then
        return 0
      fi
      cache_installed_version "go" "$GO_VERSION"
      return 0
    else
      rm -rf "$GO_INSTALL_DIR"
    fi
  fi

  msg_info "Setup Go $GO_VERSION"

  local TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
  local URL="https://go.dev/dl/${TARBALL}"
  local TMP_TAR=$(mktemp)

  curl -fsSL "$URL" -o "$TMP_TAR" || {
    msg_error "Failed to download $TARBALL"
    rm -f "$TMP_TAR"
    return 1
  }

  $STD tar -C /usr/local -xzf "$TMP_TAR" || {
    msg_error "Failed to extract Go tarball"
    rm -f "$TMP_TAR"
    return 1
  }
  ln -sf /usr/local/go/bin/go /usr/local/bin/go
  ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
  rm -f "$TMP_TAR"

  cache_installed_version "go" "$GO_VERSION"
  ensure_usr_local_bin_persist
  msg_ok "Setup Go $GO_VERSION"
}

# ------------------------------------------------------------------------------
# Installs or updates Ghostscript (gs) from source.
#
# Description:
#   - Fetches latest release
#   - Builds and installs system-wide
# ------------------------------------------------------------------------------

function setup_gs() {
  local TMP_DIR=$(mktemp -d)
  local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0")
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "ghostscript")

  ensure_dependencies jq

  local RELEASE_JSON
  RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest)
  local LATEST_VERSION
  LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^gs//')
  local LATEST_VERSION_DOTTED
  LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | jq -r '.name' | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+')

  if [[ -z "$LATEST_VERSION" || -z "$LATEST_VERSION_DOTTED" ]]; then
    msg_error "Could not determine latest Ghostscript version"
    rm -rf "$TMP_DIR"
    return 1
  fi

  if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then
    if [[ "$CACHED_VERSION" == "$LATEST_VERSION_DOTTED" ]]; then
      rm -rf "$TMP_DIR"
      return 0
    fi
    cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED"
    rm -rf "$TMP_DIR"
    return 0
  fi

  msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED"

  curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" || {
    msg_error "Failed to download Ghostscript"
    rm -rf "$TMP_DIR"
    return 1
  }

  if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then
    msg_error "Failed to extract Ghostscript archive"
    rm -rf "$TMP_DIR"
    return 1
  fi

  cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || {
    msg_error "Failed to enter Ghostscript source directory"
    rm -rf "$TMP_DIR"
    return 1
  }

  ensure_dependencies build-essential libpng-dev zlib1g-dev

  $STD ./configure || {
    msg_error "Ghostscript configure failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  $STD make -j"$(nproc)" || {
    msg_error "Ghostscript compilation failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  $STD make install || {
    msg_error "Ghostscript installation failed"
    rm -rf "$TMP_DIR"
    return 1
  }

  hash -r
  if [[ ! -x "$(command -v gs)" ]]; then
    if [[ -x /usr/local/bin/gs ]]; then
      ln -sf /usr/local/bin/gs /usr/bin/gs
    fi
  fi

  rm -rf "$TMP_DIR"
  cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED"
  ensure_usr_local_bin_persist
  msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED"
}

# ------------------------------------------------------------------------------
# Sets up Hardware Acceleration on debian or ubuntu.
#
# Description:
#   - Determites CPU/GPU/APU Vendor
#   - Installs the correct libraries and packages
#   - Sets up Hardware Acceleration
#
# Notes:
#   - Some things are fetched from intel repositories due to not being in debian repositories.
# ------------------------------------------------------------------------------
function setup_hwaccel() {
  msg_info "Setup Hardware Acceleration"

  if ! command -v lspci &>/dev/null; then
    $STD apt -y update || {
      msg_error "Failed to update package list"
      return 1
    }
    $STD apt -y install pciutils || {
      msg_error "Failed to install pciutils"
      return 1
    }
  fi

  # Detect GPU vendor (Intel, AMD, NVIDIA)
  local gpu_vendor
  gpu_vendor=$(lspci | grep -Ei 'vga|3d|display' | grep -Eo 'Intel|AMD|NVIDIA' | head -n1)

  # Detect CPU vendor (relevant for AMD APUs)
  local cpu_vendor
  cpu_vendor=$(lscpu | grep -i 'Vendor ID' | awk '{print $3}')

  if [[ -z "$gpu_vendor" && -z "$cpu_vendor" ]]; then
    msg_error "No GPU or CPU vendor detected (missing lspci/lscpu output)"
    return 1
  fi

  # Detect OS
  local os_id os_codename
  os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release | tr -d '"')
  os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release | tr -d '"')

  # Determine if we are on a VM or LXC
  local in_ct="${CTTYPE:-0}"

  case "$gpu_vendor" in
  Intel)
    if [[ "$os_id" == "ubuntu" ]]; then
      $STD apt -y install intel-opencl-icd || msg_error "Failed to install intel-opencl-icd"
    else
      fetch_and_deploy_gh_release "intel-igc-core-2" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb"
      fetch_and_deploy_gh_release "intel-igc-opencl-2" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb"
      fetch_and_deploy_gh_release "intel-libgdgmm12" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb"
      fetch_and_deploy_gh_release "intel-opencl-icd" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb"
    fi

    $STD apt -y install va-driver-all ocl-icd-libopencl1 vainfo intel-gpu-tools || msg_error "Failed to install GPU dependencies"
    ;;
  AMD)
    $STD apt -y install mesa-va-drivers mesa-vdpau-drivers mesa-opencl-icd vainfo clinfo || msg_error "Failed to install AMD GPU dependencies"

    # For AMD CPUs without discrete GPU (APUs)
    if [[ "$cpu_vendor" == "AuthenticAMD" && "$gpu_vendor" != "AMD" ]]; then
      $STD apt -y install libdrm-amdgpu1 firmware-amd-graphics || true
    fi
    ;;
  NVIDIA)
    # NVIDIA needs manual driver setup
    ;;
  *)
    # If no discrete GPU, but AMD CPU (e.g., Ryzen APU)
    if [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
      $STD apt -y install mesa-opencl-icd ocl-icd-libopencl1 clinfo || msg_error "Failed to install Mesa OpenCL stack"
    else
      msg_error "No supported GPU vendor detected"
      return 1
    fi
    ;;
  esac

  if [[ "$in_ct" == "0" ]]; then
    chgrp video /dev/dri 2>/dev/null || true
    chmod 755 /dev/dri 2>/dev/null || true
    chmod 660 /dev/dri/* 2>/dev/null || true
    $STD adduser "$(id -u -n)" video
    $STD adduser "$(id -u -n)" render
  fi

  msg_ok "Setup Hardware Acceleration"
}

# ------------------------------------------------------------------------------
# Installs ImageMagick 7 from source (Debian/Ubuntu only).
#
# Description:
#   - Downloads the latest ImageMagick source tarball
#   - Builds and installs ImageMagick to /usr/local
#   - Configures dynamic linker (ldconfig)
#
# Notes:
#   - Requires: build-essential, libtool, libjpeg-dev, libpng-dev, etc.
# ------------------------------------------------------------------------------
function setup_imagemagick() {
  local TMP_DIR=$(mktemp -d)
  local VERSION=""
  local BINARY_PATH="/usr/local/bin/magick"
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "imagemagick")

  if command -v magick &>/dev/null; then
    VERSION=$(magick -version | awk '/^Version/ {print $3}')
    if [[ "$CACHED_VERSION" == "$VERSION" ]]; then
      rm -rf "$TMP_DIR"
      return 0
    fi
    cache_installed_version "imagemagick" "$VERSION"
    rm -rf "$TMP_DIR"
    return 0
  fi

  msg_info "Setup ImageMagick"

  ensure_dependencies \
    build-essential \
    libtool \
    libjpeg-dev \
    libpng-dev \
    libtiff-dev \
    libwebp-dev \
    libheif-dev \
    libde265-dev \
    libopenjp2-7-dev \
    libxml2-dev \
    liblcms2-dev \
    libfreetype6-dev \
    libraw-dev \
    libfftw3-dev \
    liblqr-1-0-dev \
    libgsl-dev \
    pkg-config \
    ghostscript

  curl -fsSL https://imagemagick.org/archive/ImageMagick.tar.gz -o "$TMP_DIR/ImageMagick.tar.gz" || {
    msg_error "Failed to download ImageMagick"
    rm -rf "$TMP_DIR"
    return 1
  }

  tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" || {
    msg_error "Failed to extract ImageMagick"
    rm -rf "$TMP_DIR"
    return 1
  }

  cd "$TMP_DIR"/ImageMagick-* || {
    msg_error "Source extraction failed"
    rm -rf "$TMP_DIR"
    return 1
  }

  $STD ./configure --disable-static || {
    msg_error "ImageMagick configure failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  $STD make -j"$(nproc)" || {
    msg_error "ImageMagick compilation failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  $STD make install || {
    msg_error "ImageMagick installation failed"
    rm -rf "$TMP_DIR"
    return 1
  }
  $STD ldconfig /usr/local/lib

  if [[ ! -x "$BINARY_PATH" ]]; then
    msg_error "ImageMagick installation failed"
    rm -rf "$TMP_DIR"
    return 1
  fi

  VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}')
  rm -rf "$TMP_DIR"
  cache_installed_version "imagemagick" "$VERSION"
  ensure_usr_local_bin_persist
  msg_ok "Setup ImageMagick"
}

# ------------------------------------------------------------------------------
# Installs Temurin JDK via Adoptium APT repository.
#
# Description:
#   - Removes previous JDK if version mismatch
#   - Installs or upgrades to specified JAVA_VERSION
#
# Variables:
#   JAVA_VERSION   - Temurin JDK version to install (e.g. 17, 21)
# ------------------------------------------------------------------------------

function setup_java() {
  local JAVA_VERSION="${JAVA_VERSION:-21}"
  local DISTRO_ID DISTRO_CODENAME
  DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
  DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)
  local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk"

  # Check cached version
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "temurin-jdk")

  # Add repo nur wenn nötig
  if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then
    # Cleanup old repository files
    cleanup_old_repo_files "adoptium"

    # Use helper function to get fallback suite
    local SUITE
    SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb")

    # Use standardized repo setup
    setup_deb822_repo \
      "adoptium" \
      "https://packages.adoptium.net/artifactory/api/gpg/key/public" \
      "https://packages.adoptium.net/artifactory/deb" \
      "$SUITE" \
      "main" \
      "amd64 arm64"
  fi

  local INSTALLED_VERSION=""
  if dpkg -l | grep -q "temurin-.*-jdk"; then
    INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+')
  fi

  if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then
    if [[ "$CACHED_VERSION" == "$JAVA_VERSION" ]]; then
      # Already at correct version, just upgrade if available
      upgrade_package "$DESIRED_PACKAGE"
    else
      $STD apt update
      $STD apt install --only-upgrade -y "$DESIRED_PACKAGE"
      cache_installed_version "temurin-jdk" "$JAVA_VERSION"
    fi
  else
    msg_info "Setup Temurin JDK $JAVA_VERSION"
    if [[ -n "$INSTALLED_VERSION" ]]; then
      $STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk"
    fi

    $STD apt install -y "$DESIRED_PACKAGE" || {
      msg_error "Failed to install Temurin JDK $JAVA_VERSION"
      return 1
    }
    cache_installed_version "temurin-jdk" "$JAVA_VERSION"
    msg_ok "Setup Temurin JDK $JAVA_VERSION"
  fi
}

# ------------------------------------------------------------------------------
# Installs a local IP updater script using networkd-dispatcher.
#
# Description:
#   - Stores current IP in /run/local-ip.env
#   - Automatically runs on network changes
# ------------------------------------------------------------------------------

function setup_local_ip_helper() {
  local BASE_DIR="/usr/local/community-scripts/ip-management"
  local SCRIPT_PATH="$BASE_DIR/update_local_ip.sh"
  local IP_FILE="/run/local-ip.env"
  local DISPATCHER_SCRIPT="/etc/networkd-dispatcher/routable.d/10-update-local-ip.sh"

  mkdir -p "$BASE_DIR"

  # Install networkd-dispatcher if not present
  if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then
    $STD apt update || {
      msg_error "Failed to update package list"
      return 1
    }
    $STD apt install -y networkd-dispatcher || {
      msg_error "Failed to install networkd-dispatcher"
      return 1
    }
  fi

  # Write update_local_ip.sh
  cat <<'EOF' >"$SCRIPT_PATH"
#!/bin/bash
set -euo pipefail

IP_FILE="/run/local-ip.env"
mkdir -p "$(dirname "$IP_FILE")"

get_current_ip() {
    local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default")
    local ip

    for target in "${targets[@]}"; do
        if [[ "$target" == "default" ]]; then
            ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
        else
            ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
        fi
        if [[ -n "$ip" ]]; then
            echo "$ip"
            return 0
        fi
    done

    return 1
}

current_ip="$(get_current_ip)"

if [[ -z "$current_ip" ]]; then
    echo "[ERROR] Could not detect local IP" >&2
    exit 1
fi

if [[ -f "$IP_FILE" ]]; then
    source "$IP_FILE"
    [[ "$LOCAL_IP" == "$current_ip" ]] && exit 0
fi

echo "LOCAL_IP=$current_ip" > "$IP_FILE"
echo "[INFO] LOCAL_IP updated to $current_ip"
EOF

  chmod +x "$SCRIPT_PATH"

  # Install dispatcher hook
  mkdir -p "$(dirname "$DISPATCHER_SCRIPT")"
  cat <<EOF >"$DISPATCHER_SCRIPT"
#!/bin/bash
$SCRIPT_PATH
EOF

  chmod +x "$DISPATCHER_SCRIPT"
  systemctl enable -q --now networkd-dispatcher.service
}

# ------------------------------------------------------------------------------
# Installs or updates MariaDB from official repo.
#
# Description:
#   - Detects current MariaDB version and replaces it if necessary
#   - Preserves existing database data
#   - Dynamically determines latest GA version if "latest" is given
#
# Variables:
#   MARIADB_VERSION - MariaDB version to install (e.g. 10.11, latest) (default: latest)
# ------------------------------------------------------------------------------

setup_mariadb() {
  local MARIADB_VERSION="${MARIADB_VERSION:-latest}"
  local DISTRO_ID DISTRO_CODENAME
  DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
  DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)

  if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then
    msg_error "MariaDB mirror not reachable"
    return 1
  fi

  if [[ "$MARIADB_VERSION" == "latest" ]]; then
    MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ |
      grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' |
      grep -vE 'rc/|rolling/' |
      sed 's|/||' |
      sort -Vr |
      head -n1)
    if [[ -z "$MARIADB_VERSION" ]]; then
      msg_error "Could not determine latest GA MariaDB version"
      return 1
    fi
  fi

  local CURRENT_VERSION=""
  if command -v mariadb >/dev/null; then
    CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
  fi

  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "mariadb")

  if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then
    if [[ "$CACHED_VERSION" == "$MARIADB_VERSION" ]]; then
      upgrade_package mariadb-server
      upgrade_package mariadb-client
    else
      $STD apt update || {
        msg_error "Failed to update package list"
        return 1
      }
      $STD apt install --only-upgrade -y mariadb-server mariadb-client || {
        msg_error "Failed to upgrade MariaDB"
        return 1
      }
      cache_installed_version "mariadb" "$MARIADB_VERSION"
    fi
    return 0
  fi

  msg_info "Setup MariaDB $MARIADB_VERSION"

  if [[ -n "$CURRENT_VERSION" ]]; then
    $STD systemctl stop mariadb >/dev/null 2>&1 || true
    $STD apt purge -y 'mariadb*' || true
  fi

  # Ensure APT is working before proceeding
  ensure_apt_working || return 1

  # Cleanup old repository files
  cleanup_old_repo_files "mariadb"

  # Install required dependencies first (MariaDB needs these from main repos)
  local mariadb_deps=()
  for dep in gawk rsync socat libdbi-perl pv; do
    if apt-cache search "^${dep}$" 2>/dev/null | grep -q .; then
      mariadb_deps+=("$dep")
    fi
  done

  if [[ ${#mariadb_deps[@]} -gt 0 ]]; then
    $STD apt install -y "${mariadb_deps[@]}" 2>/dev/null || true
  fi

  # Use helper function to get fallback suite
  local SUITE
  SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}")

  # Use standardized repo setup
  setup_deb822_repo \
    "mariadb" \
    "https://mariadb.org/mariadb_release_signing_key.asc" \
    "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}" \
    "$SUITE" \
    "main" \
    "amd64 arm64"

  local MARIADB_MAJOR_MINOR
  MARIADB_MAJOR_MINOR=$(echo "$MARIADB_VERSION" | awk -F. '{print $1"."$2}')
  if [[ -n "$MARIADB_MAJOR_MINOR" ]]; then
    echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections
  fi

  DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || {
    cleanup_old_repo_files "mariadb"
    $STD apt update
    DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || {
      msg_error "Failed to install MariaDB packages"
      return 1
    }
  }

  cache_installed_version "mariadb" "$MARIADB_VERSION"
  msg_ok "Setup MariaDB $MARIADB_VERSION"
}

# ------------------------------------------------------------------------------
# Installs or updates MongoDB to specified major version.
#
# Description:
#   - Preserves data across installations
#   - Adds official MongoDB repo
#
# Variables:
#   MONGO_VERSION  - MongoDB major version to install (e.g. 7.0, 8.0)
# ------------------------------------------------------------------------------

function setup_mongodb() {
  local MONGO_VERSION="${MONGO_VERSION:-8.0}"
  local DISTRO_ID DISTRO_CODENAME
  DISTRO_ID=$(awk -F= '/^ID=/{ gsub(/"/,"",$2); print $2 }' /etc/os-release)
  DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release)

  # Check AVX support
  if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then
    local major="${MONGO_VERSION%%.*}"
    if ((major > 5)); then
      msg_error "MongoDB ${MONGO_VERSION} requires AVX support, which is not available on this system."
      return 1
    fi
  fi

  case "$DISTRO_ID" in
  ubuntu)
    MONGO_BASE_URL="https://repo.mongodb.org/apt/ubuntu"
    REPO_COMPONENT="multiverse"
    ;;
  debian)
    MONGO_BASE_URL="https://repo.mongodb.org/apt/debian"
    REPO_COMPONENT="main"
    ;;
  *)
    msg_error "Unsupported distribution: $DISTRO_ID"
    return 1
    ;;
  esac

  local INSTALLED_VERSION=""
  if command -v mongod >/dev/null; then
    INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2)
  fi

  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "mongodb")

  if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then
    if [[ "$CACHED_VERSION" == "$MONGO_VERSION" ]]; then
      upgrade_package mongodb-org
    else
      $STD apt update || {
        msg_error "Failed to update package list"
        return 1
      }
      $STD apt install --only-upgrade -y mongodb-org || {
        msg_error "Failed to upgrade MongoDB"
        return 1
      }
      cache_installed_version "mongodb" "$MONGO_VERSION"
    fi
    return 0
  fi

  msg_info "Setup MongoDB $MONGO_VERSION"

  if [[ -n "$INSTALLED_VERSION" ]]; then
    $STD systemctl stop mongod || true
    $STD apt purge -y mongodb-org || true
  fi

  # Cleanup old repository files
  cleanup_old_repo_files "mongodb-org-${MONGO_VERSION}"

  # Cleanup any orphaned .sources files from other apps
  cleanup_orphaned_sources

  # Use helper function to get fallback suite
  local SUITE
  SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "$MONGO_BASE_URL")

  # Use standardized repo setup
  mkdir -p /etc/apt/keyrings
  curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor --yes -o "/etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg" || {
    msg_error "Failed to download or import MongoDB GPG key"
    return 1
  }

  cat <<EOF >/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.sources
Types: deb
URIs: ${MONGO_BASE_URL}
Suites: ${SUITE}/mongodb-org/${MONGO_VERSION}
Components: ${REPO_COMPONENT}
Architectures: amd64 arm64
Signed-By: /etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg
EOF

  $STD apt update || {
    msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?"
    return 1
  }

  $STD apt install -y mongodb-org || {
    msg_error "Failed to install MongoDB packages"
    return 1
  }

  mkdir -p /var/lib/mongodb
  chown -R mongodb:mongodb /var/lib/mongodb

  $STD systemctl enable mongod
  safe_service_restart mongod
  cache_installed_version "mongodb" "$MONGO_VERSION"
  msg_ok "Setup MongoDB $MONGO_VERSION"
}

# ------------------------------------------------------------------------------
# Installs or upgrades MySQL and configures APT repo.
#
# Description:
#   - Detects existing MySQL installation
#   - Purges conflicting packages before installation
#   - Supports clean upgrade
#   - Handles Debian Trixie libaio1t64 transition
#
# Variables:
#   MYSQL_VERSION  - MySQL version to install (e.g. 5.7, 8.0) (default: 8.0)
# ------------------------------------------------------------------------------

function setup_mysql() {
  local MYSQL_VERSION="${MYSQL_VERSION:-8.0}"
  local CURRENT_VERSION=""
  local NEED_INSTALL=false
  local DISTRO_ID DISTRO_CODENAME
  DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
  DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)

  if command -v mysql >/dev/null; then
    CURRENT_VERSION="$(mysql --version | grep -oP '[0-9]+\.[0-9]+' | head -n1)"
    if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then
      NEED_INSTALL=true
    else
      if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then
        $STD apt update || {
          msg_error "Failed to update package list"
          return 1
        }
        $STD apt install --only-upgrade -y mysql-server || {
          msg_error "Failed to upgrade MySQL"
          return 1
        }
      fi
      return
    fi
  else
    NEED_INSTALL=true
  fi

  if [[ "$NEED_INSTALL" == true ]]; then
    msg_info "Setup MySQL $MYSQL_VERSION"

    # Cleanup old repository files
    cleanup_old_repo_files "mysql"

    # Determine suite - use bookworm for Debian testing/unstable
    local SUITE
    if [[ "$DISTRO_ID" == "debian" ]]; then
      case "$DISTRO_CODENAME" in
      bookworm | bullseye)
        SUITE="$DISTRO_CODENAME"
        ;;
      trixie | forky | sid)
        SUITE="bookworm"
        ;;
      *)
        SUITE="bookworm" # Fallback to bookworm for unknown Debian versions
        ;;
      esac
    else
      # For Ubuntu - use fallback detection
      SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}")
    fi

    # Stop existing MySQL if running
    $STD systemctl stop mysql 2>/dev/null || true

    # Only purge if MySQL is actually installed
    if dpkg -l 2>/dev/null | grep -q "^ii.*mysql-server"; then
      $STD apt purge -y mysql-server* mysql-client* mysql-common 2>/dev/null || true
    fi

    # Handle libaio dependency for Debian Trixie+ (time64 transition)
    if [[ "$DISTRO_ID" == "debian" ]] && [[ "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then
      # Install libaio1t64 if not present
      if ! dpkg -l libaio1t64 2>/dev/null | grep -q "^ii"; then
        $STD apt update || {
          msg_error "Failed to update package list"
          return 1
        }
        $STD apt install -y libaio1t64 || {
          msg_error "Failed to install libaio1t64"
          return 1
        }
      fi

      # Create dummy libaio1 package for dependency satisfaction
      local TEMP_DIR="/tmp/libaio1-compat-$$"
      mkdir -p "$TEMP_DIR"
      cd "$TEMP_DIR"

      # Create control file
      mkdir -p DEBIAN
      cat >DEBIAN/control <<EOF
Package: libaio1
Version: 0.3.113-99~compat
Architecture: amd64
Maintainer: Local Build
Depends: libaio1t64
Provides: libaio1
Description: Compatibility package for libaio1 -> libaio1t64 transition
 This is a transitional dummy package to satisfy dependencies on libaio1
 while actually using libaio1t64 (time64 transition).
EOF

      # Build the dummy package
      cd /tmp
      dpkg-deb -b "$TEMP_DIR" libaio1-compat.deb >/dev/null 2>&1

      # Install it
      $STD dpkg -i libaio1-compat.deb

      # Cleanup
      rm -rf "$TEMP_DIR" libaio1-compat.deb
    fi

    # Use standardized repo setup
    setup_deb822_repo \
      "mysql" \
      "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" \
      "https://repo.mysql.com/apt/${DISTRO_ID}" \
      "$SUITE" \
      "mysql-${MYSQL_VERSION}" \
      "amd64 arm64"

    export DEBIAN_FRONTEND=noninteractive

    # Update apt
    if ! $STD apt update; then
      msg_error "APT update failed for MySQL repository"
      return 1
    fi

    # Try multiple MySQL package patterns
    local mysql_install_success=false

    # First try: mysql-server (most common)
    if apt-cache search "^mysql-server$" 2>/dev/null | grep -q . && $STD apt install -y mysql-server mysql-client 2>/dev/null; then
      mysql_install_success=true
    fi

    # Second try: mysql-community-server (when official repo)
    if [[ "$mysql_install_success" == false ]] && apt-cache search "^mysql-community-server$" 2>/dev/null | grep -q . && $STD apt install -y mysql-community-server mysql-community-client 2>/dev/null; then
      mysql_install_success=true
    fi

    # Third try: just mysql meta package
    if [[ "$mysql_install_success" == false ]] && apt-cache search "^mysql$" 2>/dev/null | grep -q . && $STD apt install -y mysql 2>/dev/null; then
      mysql_install_success=true
    fi

    if [[ "$mysql_install_success" == false ]]; then
      msg_error "MySQL ${MYSQL_VERSION} package not available for suite ${SUITE}"
      return 1
    fi

    # Verify installation
    if ! command -v mysql >/dev/null 2>&1; then
      hash -r
      if ! command -v mysql >/dev/null 2>&1; then
        msg_error "MySQL installed but mysql command still not found"
        return 1
      fi
    fi

    cache_installed_version "mysql" "$MYSQL_VERSION"
    msg_ok "Setup MySQL $MYSQL_VERSION"
  fi
}

# ------------------------------------------------------------------------------
# Installs Node.js and optional global modules.
#
# Description:
#   - Installs specified Node.js version using NodeSource APT repo
#   - Optionally installs or updates global npm modules
#
# Variables:
#   NODE_VERSION   - Node.js version to install (default: 22)
#   NODE_MODULE    - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0")
# ------------------------------------------------------------------------------

function setup_nodejs() {
  local NODE_VERSION="${NODE_VERSION:-22}"
  local NODE_MODULE="${NODE_MODULE:-}"
  local CURRENT_NODE_VERSION=""
  local NEED_NODE_INSTALL=false
  local DISTRO_ID DISTRO_CODENAME
  DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
  DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)

  if command -v node >/dev/null; then
    CURRENT_NODE_VERSION="$(node -v | grep -oP '^v\K[0-9]+')"
    if [[ "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then
      NEED_NODE_INSTALL=true
    fi
  else
    NEED_NODE_INSTALL=true
  fi

  if [[ "$NEED_NODE_INSTALL" == true ]]; then
    msg_info "Setup Node.js $NODE_VERSION"

    ensure_dependencies jq

    $STD apt purge -y nodejs

    # Cleanup old repository files
    cleanup_old_repo_files "nodesource"

    # NodeSource uses 'nodistro' for all distributions - no fallback needed
    # Use standardized repo setup
    setup_deb822_repo \
      "nodesource" \
      "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \
      "https://deb.nodesource.com/node_${NODE_VERSION}.x" \
      "nodistro" \
      "main" \
      "amd64 arm64"

    if ! $STD apt install -y nodejs; then
      msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource"
      return 1
    fi

    $STD npm install -g npm@latest 2>/dev/null || true

    cache_installed_version "nodejs" "$NODE_VERSION"
    msg_ok "Setup Node.js $NODE_VERSION"
  fi

  export NODE_OPTIONS="--max-old-space-size=4096"

  if [[ ! -d /opt ]]; then
    mkdir -p /opt
  fi
  cd /opt || {
    msg_error "Failed to set safe working directory before npm install"
    return 1
  }

  if [[ -n "$NODE_MODULE" ]]; then
    IFS=',' read -ra MODULES <<<"$NODE_MODULE"
    for mod in "${MODULES[@]}"; do
      local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION
      if [[ "$mod" == @*/*@* ]]; then
        MODULE_NAME="${mod%@*}"
        MODULE_REQ_VERSION="${mod##*@}"
      elif [[ "$mod" == *"@"* ]]; then
        MODULE_NAME="${mod%@*}"
        MODULE_REQ_VERSION="${mod##*@}"
      else
        MODULE_NAME="$mod"
        MODULE_REQ_VERSION="latest"
      fi

      if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then
        MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')"
        if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then
          if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then
            msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION"
            return 1
          fi
        elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then
          if ! $STD npm install -g "${MODULE_NAME}@latest"; then
            msg_error "Failed to update $MODULE_NAME to latest version"
            return 1
          fi
        fi
      else
        if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then
          msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION"
          return 1
        fi
      fi
    done
  fi
}

# ------------------------------------------------------------------------------
# Installs PHP with selected modules and configures Apache/FPM support.
#
# Description:
#   - Adds Sury PHP repo if needed
#   - Installs default and user-defined modules
#   - Patches php.ini for CLI, Apache, and FPM as needed
#
# Variables:
#   PHP_VERSION                - PHP version to install (default: 8.4)
#   PHP_MODULE                 - Additional comma-separated modules
#   PHP_APACHE                 - Set YES to enable PHP with Apache
#   PHP_FPM                    - Set YES to enable PHP-FPM
#   PHP_MEMORY_LIMIT           - (default: 512M)
#   PHP_UPLOAD_MAX_FILESIZE    - (default: 128M)
#   PHP_POST_MAX_SIZE          - (default: 128M)
#   PHP_MAX_EXECUTION_TIME     - (default: 300)
# ------------------------------------------------------------------------------

function setup_php() {
  local PHP_VERSION="${PHP_VERSION:-8.4}"
  local PHP_MODULE="${PHP_MODULE:-}"
  local PHP_APACHE="${PHP_APACHE:-NO}"
  local PHP_FPM="${PHP_FPM:-NO}"
  local DISTRO_ID DISTRO_CODENAME
  DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
  DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)

  local DEFAULT_MODULES="bcmath,cli,curl,gd,intl,mbstring,opcache,readline,xml,zip"
  local COMBINED_MODULES

  local PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}"
  local PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-128M}"
  local PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-128M}"
  local PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}"

  # Merge default + user-defined modules
  if [[ -n "$PHP_MODULE" ]]; then
    COMBINED_MODULES="${DEFAULT_MODULES},${PHP_MODULE}"
  else
    COMBINED_MODULES="${DEFAULT_MODULES}"
  fi

  # Deduplicate
  COMBINED_MODULES=$(echo "$COMBINED_MODULES" | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -)

  # Get current PHP-CLI version
  local CURRENT_PHP=""
  if command -v php >/dev/null 2>&1; then
    CURRENT_PHP=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2)
  fi

  msg_info "Setup PHP $PHP_VERSION"

  if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
    $STD apt purge -y "php${CURRENT_PHP//./}"* || true
  fi

  # Ensure Sury repo is available
  if [[ ! -f /etc/apt/sources.list.d/php.sources ]]; then
    # Cleanup old repository files
    cleanup_old_repo_files "php"

    $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb || {
      msg_error "Failed to download PHP repository keyring"
      return 1
    }
    $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb || {
      msg_error "Failed to install PHP repository keyring"
      rm -f /tmp/debsuryorg-archive-keyring.deb
      return 1
    }

    # Use helper function to get fallback suite
    local SUITE
    SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.sury.org/php")

    cat <<EOF >/etc/apt/sources.list.d/php.sources
Types: deb
URIs: https://packages.sury.org/php/
Suites: $SUITE
Components: main
Architectures: amd64 arm64
Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg
EOF
    $STD apt update || {
      msg_error "APT update failed for PHP repository"
      return 1
    }
  fi

  # Build module list
  local MODULE_LIST="php${PHP_VERSION}"
  IFS=',' read -ra MODULES <<<"$COMBINED_MODULES"
  for mod in "${MODULES[@]}"; do
    if apt-cache show "php${PHP_VERSION}-${mod}" >/dev/null 2>&1; then
      MODULE_LIST+=" php${PHP_VERSION}-${mod}"
    fi
  done
  if [[ "$PHP_FPM" == "YES" ]]; then
    MODULE_LIST+=" php${PHP_VERSION}-fpm"
  fi

  # install apache2 with PHP support if requested
  if [[ "$PHP_APACHE" == "YES" ]]; then
    if ! dpkg -l | grep -q "libapache2-mod-php${PHP_VERSION}"; then
      $STD apt install -y apache2 libapache2-mod-php${PHP_VERSION} || {
        msg_error "Failed to install Apache with PHP module"
        return 1
      }
    fi
  fi

  # setup / update PHP modules
  $STD apt install -y $MODULE_LIST || {
    msg_error "Failed to install PHP packages"
    return 1
  }
  cache_installed_version "php" "$PHP_VERSION"

  # optional stop old PHP-FPM service
  if [[ "$PHP_FPM" == "YES" && -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
    $STD systemctl stop php"${CURRENT_PHP}"-fpm || true
    $STD systemctl disable php"${CURRENT_PHP}"-fpm || true
  fi

  # Patch all relevant php.ini files (silent)
  local PHP_INI_PATHS=("/etc/php/${PHP_VERSION}/cli/php.ini")
  [[ "$PHP_FPM" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/fpm/php.ini")
  [[ "$PHP_APACHE" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/apache2/php.ini")
  for ini in "${PHP_INI_PATHS[@]}"; do
    if [[ -f "$ini" ]]; then
      $STD sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini"
      $STD sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini"
      $STD sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini"
      $STD sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini"
    fi
  done

  # patch Apache configuration if needed
  if [[ "$PHP_APACHE" == "YES" ]]; then
    for mod in $(ls /etc/apache2/mods-enabled/ 2>/dev/null | grep -E '^php[0-9]\.[0-9]\.conf$' | sed 's/\.conf//'); do
      if [[ "$mod" != "php${PHP_VERSION}" ]]; then
        $STD a2dismod "$mod" || true
      fi
    done
    $STD a2enmod mpm_prefork
    $STD a2enmod "php${PHP_VERSION}"
    safe_service_restart apache2 || true
  fi

  # enable and restart PHP-FPM if requested
  if [[ "$PHP_FPM" == "YES" ]]; then
    if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm.service"; then
      $STD systemctl enable php${PHP_VERSION}-fpm
      safe_service_restart php${PHP_VERSION}-fpm
    fi
  fi

  msg_ok "Setup PHP $PHP_VERSION"
}

# ------------------------------------------------------------------------------
# Installs or upgrades PostgreSQL and optional extensions/modules.
#
# Description:
#   - Detects existing PostgreSQL version
#   - Dumps all databases before upgrade
#   - Adds PGDG repo and installs specified version
#   - Installs optional PG_MODULES (e.g. postgis, contrib)
#   - Restores dumped data post-upgrade
#
# Variables:
#   PG_VERSION     - Major PostgreSQL version (e.g. 15, 16) (default: 16)
#   PG_MODULES     - Comma-separated list of extensions (e.g. "postgis,contrib")
# ------------------------------------------------------------------------------
function setup_postgresql() {
  local PG_VERSION="${PG_VERSION:-16}"
  local PG_MODULES="${PG_MODULES:-}"
  local CURRENT_PG_VERSION=""
  local DISTRO_ID DISTRO_CODENAME
  local NEED_PG_INSTALL=false
  DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
  DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)

  if command -v psql >/dev/null; then
    CURRENT_PG_VERSION="$(psql -V | awk '{print $3}' | cut -d. -f1)"
    [[ "$CURRENT_PG_VERSION" != "$PG_VERSION" ]] && NEED_PG_INSTALL=true
  else
    NEED_PG_INSTALL=true
  fi

  if [[ "$NEED_PG_INSTALL" == true ]]; then
    msg_info "Setup PostgreSQL $PG_VERSION"

    if [[ -n "$CURRENT_PG_VERSION" ]]; then
      $STD runuser -u postgres -- pg_dumpall >/var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql
      $STD systemctl stop postgresql
    fi

    # Cleanup old repository files
    cleanup_old_repo_files "pgdg"

    # PostgreSQL PGDG repository uses special suite naming
    # For unstable/testing Debian, we need to check what's actually available
    local SUITE
    case "$DISTRO_CODENAME" in
    trixie | forky | sid)
      # Try trixie-pgdg first, fallback to bookworm-pgdg if not available
      if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then
        SUITE="trixie-pgdg"
      else
        SUITE="bookworm-pgdg"
      fi
      ;;
    *)
      # Use helper function for stable releases
      SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt")
      # PGDG uses special suite naming: ${SUITE}-pgdg
      SUITE="${SUITE}-pgdg"
      ;;
    esac

    # Use standardized repo setup
    setup_deb822_repo \
      "pgdg" \
      "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \
      "https://apt.postgresql.org/pub/repos/apt" \
      "$SUITE" \
      "main" \
      "amd64 arm64"

    # Update apt
    if ! $STD apt update; then
      msg_error "APT update failed for PostgreSQL repository"
      return 1
    fi

    # Try multiple PostgreSQL package patterns
    local pg_install_success=false

    # First, ensure ssl-cert is available (PostgreSQL dependency)
    if apt-cache search "^ssl-cert$" 2>/dev/null | grep -q .; then
      $STD apt install -y ssl-cert 2>/dev/null || true
    fi

    # First try: postgresql-X (common in most repos)
    if apt-cache search "^postgresql-${PG_VERSION}$" 2>/dev/null | grep -q . && $STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null; then
      pg_install_success=true
    fi

    # Second try: postgresql-server-X (some repos use this)
    if [[ "$pg_install_success" == false ]] && apt-cache search "^postgresql-server-${PG_VERSION}$" 2>/dev/null | grep -q . && $STD apt install -y "postgresql-server-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null; then
      pg_install_success=true
    fi

    # Third try: just postgresql (any version)
    if [[ "$pg_install_success" == false ]] && apt-cache search "^postgresql$" 2>/dev/null | grep -q . && $STD apt install -y postgresql postgresql-client 2>/dev/null; then
      pg_install_success=true
    fi

    if [[ "$pg_install_success" == false ]]; then
      msg_error "PostgreSQL package not available for suite ${SUITE}"
      return 1
    fi

    # Verify installation
    if ! command -v psql >/dev/null 2>&1; then
      msg_error "PostgreSQL installed but psql command not found"
      return 1
    fi

    if [[ -n "$CURRENT_PG_VERSION" ]]; then
      $STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" 2>/dev/null || true
      $STD runuser -u postgres -- psql </var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql 2>/dev/null || true
    fi

    $STD systemctl enable --now postgresql 2>/dev/null || true

    # Add PostgreSQL binaries to PATH for the install script
    if ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then
      echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/'"${PG_VERSION}"'/bin"' >/etc/environment
    fi

    cache_installed_version "postgresql" "$PG_VERSION"
    msg_ok "Setup PostgreSQL $PG_VERSION"
  fi

  if [[ -n "$PG_MODULES" ]]; then
    IFS=',' read -ra MODULES <<<"$PG_MODULES"
    for module in "${MODULES[@]}"; do
      $STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true
    done
  fi
}

# ------------------------------------------------------------------------------
# Installs rbenv and ruby-build, installs Ruby and optionally Rails.
#
# Description:
#   - Downloads rbenv and ruby-build from GitHub
#   - Compiles and installs target Ruby version
#   - Optionally installs Rails via gem
#
# Variables:
#   RUBY_VERSION         - Ruby version to install (default: 3.4.4)
#   RUBY_INSTALL_RAILS   - true/false to install Rails (default: true)
# ------------------------------------------------------------------------------

function setup_ruby() {
  local RUBY_VERSION="${RUBY_VERSION:-3.4.4}"
  local RUBY_INSTALL_RAILS="${RUBY_INSTALL_RAILS:-true}"
  local RBENV_DIR="$HOME/.rbenv"
  local RBENV_BIN="$RBENV_DIR/bin/rbenv"
  local PROFILE_FILE="$HOME/.profile"
  local TMP_DIR=$(mktemp -d)
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "ruby")

  msg_info "Setup Ruby $RUBY_VERSION"

  # Ensure APT is working before installing dependencies
  ensure_apt_working || return 1

  # Install build dependencies - with fallback for different Debian versions
  # In Trixie: libreadline6-dev -> libreadline-dev, libncurses5-dev -> libncurses-dev
  local ruby_deps=()
  local dep_variations=(
    "jq"
    "autoconf"
    "patch"
    "build-essential"
    "libssl-dev"
    "libyaml-dev"
    "libreadline-dev|libreadline6-dev"
    "zlib1g-dev"
    "libgmp-dev"
    "libncurses-dev|libncurses5-dev"
    "libffi-dev"
    "libgdbm-dev"
    "libdb-dev"
    "uuid-dev"
  )

  for dep_pattern in "${dep_variations[@]}"; do
    if [[ "$dep_pattern" == *"|"* ]]; then
      # Try multiple variations
      IFS='|' read -ra variations <<<"$dep_pattern"
      local found=false
      for var in "${variations[@]}"; do
        if apt-cache search "^${var}$" 2>/dev/null | grep -q .; then
          ruby_deps+=("$var")
          found=true
          break
        fi
      done
    else
      if apt-cache search "^${dep_pattern}$" 2>/dev/null | grep -q .; then
        ruby_deps+=("$dep_pattern")
      fi
    fi
  done

  if [[ ${#ruby_deps[@]} -gt 0 ]]; then
    $STD apt install -y "${ruby_deps[@]}" 2>/dev/null || true
  else
    msg_error "No Ruby build dependencies available"
    return 1
  fi

  local RBENV_RELEASE
  RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | jq -r '.tag_name' | sed 's/^v//')
  if [[ -z "$RBENV_RELEASE" ]]; then
    msg_error "Failed to fetch latest rbenv version"
    rm -rf "$TMP_DIR"
    return 1
  fi

  curl -fsSL "https://github.com/rbenv/rbenv/archive/refs/tags/v${RBENV_RELEASE}.tar.gz" -o "$TMP_DIR/rbenv.tar.gz" || {
    msg_error "Failed to download rbenv"
    rm -rf "$TMP_DIR"
    return 1
  }

  tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" || {
    msg_error "Failed to extract rbenv"
    rm -rf "$TMP_DIR"
    return 1
  }

  mkdir -p "$RBENV_DIR"
  cp -r "$TMP_DIR/rbenv-${RBENV_RELEASE}/." "$RBENV_DIR/"
  cd "$RBENV_DIR" && src/configure && $STD make -C src || {
    msg_error "Failed to build rbenv"
    rm -rf "$TMP_DIR"
    return 1
  }

  local RUBY_BUILD_RELEASE
  RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest | jq -r '.tag_name' | sed 's/^v//')
  if [[ -z "$RUBY_BUILD_RELEASE" ]]; then
    msg_error "Failed to fetch latest ruby-build version"
    rm -rf "$TMP_DIR"
    return 1
  fi

  curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/v${RUBY_BUILD_RELEASE}.tar.gz" -o "$TMP_DIR/ruby-build.tar.gz" || {
    msg_error "Failed to download ruby-build"
    rm -rf "$TMP_DIR"
    return 1
  }

  tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" || {
    msg_error "Failed to extract ruby-build"
    rm -rf "$TMP_DIR"
    return 1
  }

  mkdir -p "$RBENV_DIR/plugins/ruby-build"
  cp -r "$TMP_DIR/ruby-build-${RUBY_BUILD_RELEASE}/." "$RBENV_DIR/plugins/ruby-build/"
  echo "$RUBY_BUILD_RELEASE" >"$RBENV_DIR/plugins/ruby-build/RUBY_BUILD_version.txt"

  if ! grep -q 'rbenv init' "$PROFILE_FILE"; then
    echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE"
    echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE"
  fi

  export PATH="$RBENV_DIR/bin:$PATH"
  eval "$("$RBENV_BIN" init - bash)"

  if ! "$RBENV_BIN" versions --bare | grep -qx "$RUBY_VERSION"; then
    $STD "$RBENV_BIN" install "$RUBY_VERSION" || {
      msg_error "Failed to install Ruby $RUBY_VERSION"
      rm -rf "$TMP_DIR"
      return 1
    }
  fi

  "$RBENV_BIN" global "$RUBY_VERSION"
  hash -r

  if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then
    $STD gem install rails || {
      msg_error "Failed to install Rails"
      rm -rf "$TMP_DIR"
      return 1
    }
  fi

  rm -rf "$TMP_DIR"
  cache_installed_version "ruby" "$RUBY_VERSION"
  msg_ok "Setup Ruby $RUBY_VERSION"
}

# ------------------------------------------------------------------------------
# Installs or upgrades ClickHouse database server.
#
# Description:
#   - Adds ClickHouse official repository
#   - Installs specified version
#   - Configures systemd service
#   - Supports Debian/Ubuntu with fallback mechanism
#
# Variables:
#   CLICKHOUSE_VERSION  - ClickHouse version to install (default: latest)
# ------------------------------------------------------------------------------

function setup_clickhouse() {
  local CLICKHOUSE_VERSION="${CLICKHOUSE_VERSION:-latest}"
  local DISTRO_ID DISTRO_CODENAME
  DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
  DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)

  # Determine latest version if needed
  if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then
    # Try multiple methods to get the latest version
    CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null |
      grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
      sort -V |
      tail -n1)

    # Fallback: Try GitHub releases API
    if [[ -z "$CLICKHOUSE_VERSION" ]]; then
      CLICKHOUSE_VERSION=$(curl -fsSL https://api.github.com/repos/ClickHouse/ClickHouse/releases/latest 2>/dev/null |
        grep -oP '"tag_name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
        head -n1)
    fi

    # Final fallback: Parse HTML more liberally
    if [[ -z "$CLICKHOUSE_VERSION" ]]; then
      CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null |
        grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(?=/clickhouse)' |
        sort -V |
        tail -n1)
    fi

    if [[ -z "$CLICKHOUSE_VERSION" ]]; then
      msg_error "Could not determine latest ClickHouse version"
      return 1
    fi
  fi

  local CURRENT_VERSION=""
  if command -v clickhouse-server >/dev/null 2>&1; then
    CURRENT_VERSION=$(clickhouse-server --version 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
  fi

  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "clickhouse")

  # Check if already at target version
  if [[ "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
    if [[ "$CACHED_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
      upgrade_package clickhouse-server
      upgrade_package clickhouse-client
    else
      msg_info "Setup ClickHouse $CLICKHOUSE_VERSION"
      $STD apt update || {
        msg_error "Failed to update package list"
        return 1
      }
      $STD apt install --only-upgrade -y clickhouse-server clickhouse-client || {
        msg_error "Failed to upgrade ClickHouse"
        return 1
      }
      cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION"
      msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION"
    fi
    return 0
  fi

  msg_info "Setup ClickHouse $CLICKHOUSE_VERSION"

  # Stop existing service if upgrading
  if [[ -n "$CURRENT_VERSION" ]]; then
    $STD systemctl stop clickhouse-server || true
  fi

  # Cleanup old repository files
  cleanup_old_repo_files "clickhouse"

  # Ensure dependencies
  ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg

  # ClickHouse uses 'stable' instead of distro codenames
  local SUITE="stable"

  # Use standardized repo setup
  setup_deb822_repo \
    "clickhouse" \
    "https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \
    "https://packages.clickhouse.com/deb" \
    "$SUITE" \
    "main" \
    "amd64 arm64"

  # Update and install ClickHouse packages
  export DEBIAN_FRONTEND=noninteractive
  if ! $STD apt update; then
    msg_error "APT update failed for ClickHouse repository"
    return 1
  fi

  if ! $STD apt install -y clickhouse-server clickhouse-client; then
    msg_error "Failed to install ClickHouse packages"
    return 1
  fi

  # Verify installation
  if ! command -v clickhouse-server >/dev/null 2>&1; then
    msg_error "ClickHouse installation completed but clickhouse-server command not found"
    return 1
  fi

  # Create data directory if it doesn't exist
  mkdir -p /var/lib/clickhouse

  # Check if clickhouse user exists before chown
  if id clickhouse >/dev/null 2>&1; then
    chown -R clickhouse:clickhouse /var/lib/clickhouse
  fi

  # Enable and start service
  $STD systemctl enable clickhouse-server
  safe_service_restart clickhouse-server || true

  cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION"
  msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION"
}

# ------------------------------------------------------------------------------
# Installs Rust toolchain and optional global crates via cargo.
#
# Description:
#   - Installs rustup (if missing)
#   - Installs or updates desired Rust toolchain (stable, nightly, or versioned)
#   - Installs or updates specified global crates using `cargo install`
#
# Notes:
#   - Skips crate install if exact version is already present
#   - Updates crate if newer version or different version is requested
#
# Variables:
#   RUST_TOOLCHAIN  - Rust toolchain to install (default: stable)
#   RUST_CRATES     - Comma-separated list of crates (e.g. "cargo-edit,wasm-pack@0.12.1")
# ------------------------------------------------------------------------------

function setup_rust() {
  local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}"
  local RUST_CRATES="${RUST_CRATES:-}"
  local CARGO_BIN="${HOME}/.cargo/bin"
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "rust")

  if ! command -v rustup &>/dev/null; then
    msg_info "Setup Rust"
    curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" || {
      msg_error "Failed to install Rust"
      return 1
    }
    export PATH="$CARGO_BIN:$PATH"
    echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>"$HOME/.profile"
    local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}')
    cache_installed_version "rust" "$RUST_VERSION"
    msg_ok "Setup Rust $RUST_VERSION"
  else
    msg_info "Setup Rust"
    $STD rustup install "$RUST_TOOLCHAIN" || {
      msg_error "Failed to install Rust toolchain"
      return 1
    }
    $STD rustup default "$RUST_TOOLCHAIN" || {
      msg_error "Failed to set default Rust toolchain"
      return 1
    }
    $STD rustup update "$RUST_TOOLCHAIN"
    local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}')
    cache_installed_version "rust" "$RUST_VERSION"
    msg_ok "Setup Rust $RUST_VERSION"
  fi

  if [[ -n "$RUST_CRATES" ]]; then
    IFS=',' read -ra CRATES <<<"$RUST_CRATES"
    for crate in "${CRATES[@]}"; do
      local NAME VER INSTALLED_VER
      if [[ "$crate" == *"@"* ]]; then
        NAME="${crate%@*}"
        VER="${crate##*@}"
      else
        NAME="$crate"
        VER=""
      fi

      INSTALLED_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v')

      if [[ -n "$INSTALLED_VER" ]]; then
        if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then
          $STD cargo install "$NAME" --version "$VER" --force
        elif [[ -z "$VER" ]]; then
          $STD cargo install "$NAME" --force
        fi
      else
        $STD cargo install "$NAME" ${VER:+--version "$VER"}
      fi
    done
  fi
}

# ------------------------------------------------------------------------------
# Installs or upgrades uv (Python package manager) from GitHub releases.
#   - Downloads platform-specific tarball (no install.sh!)
#   - Extracts uv binary
#   - Places it in /usr/local/bin
#   - Optionally installs a specific Python version via uv
# ------------------------------------------------------------------------------

function setup_uv() {
  local UV_BIN="/usr/local/bin/uv"
  local TMP_DIR=$(mktemp -d)
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "uv")

  local ARCH=$(uname -m)
  local UV_TAR

  case "$ARCH" in
  x86_64)
    if grep -qi "alpine" /etc/os-release; then
      UV_TAR="uv-x86_64-unknown-linux-musl.tar.gz"
    else
      UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz"
    fi
    ;;
  aarch64)
    if grep -qi "alpine" /etc/os-release; then
      UV_TAR="uv-aarch64-unknown-linux-musl.tar.gz"
    else
      UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz"
    fi
    ;;
  *)
    msg_error "Unsupported architecture: $ARCH"
    rm -rf "$TMP_DIR"
    return 1
    ;;
  esac

  ensure_dependencies jq

  local LATEST_VERSION
  LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest |
    jq -r '.tag_name' | sed 's/^v//')

  if [[ -z "$LATEST_VERSION" ]]; then
    msg_error "Could not fetch latest uv version"
    rm -rf "$TMP_DIR"
    return 1
  fi

  if [[ -x "$UV_BIN" ]]; then
    local INSTALLED_VERSION
    INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}')
    if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then
      if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then
        rm -rf "$TMP_DIR"
        return 0
      fi
      cache_installed_version "uv" "$LATEST_VERSION"
      rm -rf "$TMP_DIR"
      return 0
    fi
  fi

  msg_info "Setup uv $LATEST_VERSION"

  local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}"
  curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || {
    msg_error "Failed to download uv"
    rm -rf "$TMP_DIR"
    return 1
  }

  tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || {
    msg_error "Failed to extract uv"
    rm -rf "$TMP_DIR"
    return 1
  }

  install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || {
    msg_error "Failed to install uv binary"
    rm -rf "$TMP_DIR"
    return 1
  }

  rm -rf "$TMP_DIR"
  ensure_usr_local_bin_persist
  export PATH="/usr/local/bin:$PATH"

  $STD uv python update-shell || true
  cache_installed_version "uv" "$LATEST_VERSION"
  msg_ok "Setup uv $LATEST_VERSION"

  if [[ -n "${PYTHON_VERSION:-}" ]]; then
    local VERSION_MATCH
    VERSION_MATCH=$(uv python list --only-downloads |
      grep -E "^cpython-${PYTHON_VERSION//./\\.}\.[0-9]+-linux" |
      cut -d'-' -f2 | sort -V | tail -n1)

    if [[ -z "$VERSION_MATCH" ]]; then
      msg_error "No matching Python $PYTHON_VERSION.x version found"
      return 1
    fi

    if ! uv python list | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then
      $STD uv python install "$VERSION_MATCH" || {
        msg_error "Failed to install Python $VERSION_MATCH"
        return 1
      }
    fi
  fi
}

# ------------------------------------------------------------------------------
# Installs or updates yq (mikefarah/yq - Go version).
#
# Description:
#   - Checks if yq is installed and from correct source
#   - Compares with latest release on GitHub
#   - Updates if outdated or wrong implementation
# ------------------------------------------------------------------------------

function setup_yq() {
  local TMP_DIR=$(mktemp -d)
  local BINARY_PATH="/usr/local/bin/yq"
  local GITHUB_REPO="mikefarah/yq"
  local CURRENT_VERSION=""
  local CACHED_VERSION
  CACHED_VERSION=$(get_cached_version "yq")

  ensure_dependencies jq
  ensure_usr_local_bin_persist

  if command -v yq &>/dev/null; then
    if ! yq --version 2>&1 | grep -q 'mikefarah'; then
      rm -f "$(command -v yq)"
    else
      CURRENT_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
    fi
  fi

  local LATEST_VERSION
  LATEST_VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" |
    jq -r '.tag_name' | sed 's/^v//')

  if [[ -z "$LATEST_VERSION" ]]; then
    msg_error "Could not determine latest yq version"
    rm -rf "$TMP_DIR"
    return 1
  fi

  if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then
    if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then
      rm -rf "$TMP_DIR"
      return 0
    fi
    cache_installed_version "yq" "$LATEST_VERSION"
    rm -rf "$TMP_DIR"
    return 0
  fi

  msg_info "Setup yq $LATEST_VERSION"
  curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" || {
    msg_error "Failed to download yq"
    rm -rf "$TMP_DIR"
    return 1
  }

  chmod +x "$TMP_DIR/yq"
  mv "$TMP_DIR/yq" "$BINARY_PATH"

  if [[ ! -x "$BINARY_PATH" ]]; then
    msg_error "Failed to install yq"
    rm -rf "$TMP_DIR"
    return 1
  fi

  rm -rf "$TMP_DIR"
  hash -r

  local FINAL_VERSION
  FINAL_VERSION=$("$BINARY_PATH" --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
  cache_installed_version "yq" "$FINAL_VERSION"
  msg_ok "Setup yq $FINAL_VERSION"
}
