# Copyright (c) 2021-2026 community-scripts ORG
# Author: michelroegl-brunner | MickLesk
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE

# ==============================================================================
# API.FUNC - TELEMETRY & DIAGNOSTICS API
# ==============================================================================
#
# Provides functions for sending anonymous telemetry data via the community
# telemetry ingest service at telemetry.community-scripts.org.
#
# Features:
#   - Container/VM creation statistics
#   - Installation success/failure tracking
#   - Error code mapping and reporting
#   - Privacy-respecting anonymous telemetry
#
# Usage:
#   source <(curl -fsSL .../api.func)
#   post_to_api          # Report LXC container creation
#   post_to_api_vm       # Report VM creation
#   post_update_to_api   # Report installation status
#
# Privacy:
#   - Only anonymous statistics (no personal data)
#   - User can opt-out via DIAGNOSTICS=no
#   - Random UUID for session tracking only
#   - Data retention: 30 days
#
# ==============================================================================

# ==============================================================================
# Telemetry Configuration
# ==============================================================================
TELEMETRY_URL="https://telemetry.community-scripts.org/telemetry"

# Timeout for telemetry requests (seconds)
# Progress pings (validation/configuring) use the short timeout
TELEMETRY_TIMEOUT=5
# Final status updates (success/failed) use the longer timeout
# PocketBase may need more time under load (FindRecord + UpdateRecord)
STATUS_TIMEOUT=10

# ==============================================================================
# SECTION 0: REPOSITORY SOURCE DETECTION
# ==============================================================================

# ------------------------------------------------------------------------------
# detect_repo_source()
#
# - Dynamically detects which GitHub/Gitea repo the scripts were loaded from
# - Inspects /proc/$$/cmdline and $0 to find the source URL
# - Maps detected repo to one of three canonical values:
#   * "ProxmoxVE"  — official community-scripts/ProxmoxVE (production)
#   * "ProxmoxVED" — official community-scripts/ProxmoxVED (development)
#   * "external"   — any fork or unknown source
# - Fallback: "ProxmoxVED" (CI sed transforms ProxmoxVED → ProxmoxVE on promotion)
# - Sets and exports REPO_SOURCE global variable
# - Skips detection if REPO_SOURCE is already set (e.g., by environment)
# ------------------------------------------------------------------------------
detect_repo_source() {
  # Allow explicit override via environment
  [[ -n "${REPO_SOURCE:-}" ]] && return 0

  local content="" owner_repo=""

  # Method 1: Read from /proc/$$/cmdline
  # When invoked via: bash -c "$(curl -fsSL https://.../ct/app.sh)"
  # the full CT/VM script content is in /proc/$$/cmdline (same PID through source chain)
  if [[ -r /proc/$$/cmdline ]]; then
    content=$(tr '\0' ' ' </proc/$$/cmdline 2>/dev/null) || true
  fi

  # Method 2: Read from the original script file (bash ct/app.sh / bash vm/app.sh)
  if [[ -z "$content" ]] || ! echo "$content" | grep -qE 'githubusercontent\.com|community-scripts\.org' 2>/dev/null; then
    if [[ -f "$0" ]] && [[ "$0" != *bash* ]]; then
      content=$(head -10 "$0" 2>/dev/null) || true
    fi
  fi

  # Extract owner/repo from URL patterns found in the script content
  if [[ -n "$content" ]]; then
    # GitHub raw URL: raw.githubusercontent.com/OWNER/REPO/...
    owner_repo=$(echo "$content" | grep -oE 'raw\.githubusercontent\.com/[^/]+/[^/]+' | head -1 | sed 's|raw\.githubusercontent\.com/||') || true

    # Gitea URL: git.community-scripts.org/OWNER/REPO/...
    if [[ -z "$owner_repo" ]]; then
      owner_repo=$(echo "$content" | grep -oE 'git\.community-scripts\.org/[^/]+/[^/]+' | head -1 | sed 's|git\.community-scripts\.org/||') || true
    fi
  fi

  # Map detected owner/repo to canonical repo_source value
  case "$owner_repo" in
  community-scripts/ProxmoxVE) REPO_SOURCE="ProxmoxVE" ;;
  community-scripts/ProxmoxVED) REPO_SOURCE="ProxmoxVED" ;;
  "")
    # No URL detected — use hardcoded fallback
    # This value must match the repo: ProxmoxVE for production, ProxmoxVED for dev
    REPO_SOURCE="ProxmoxVE"
    ;;
  *)
    # Fork or unknown repo
    REPO_SOURCE="external"
    ;;
  esac

  export REPO_SOURCE
}

# Run detection immediately when api.func is sourced
detect_repo_source

# ==============================================================================
# SECTION 1: ERROR CODE DESCRIPTIONS
# ==============================================================================

# ------------------------------------------------------------------------------
# explain_exit_code()
#
# - Maps numeric exit codes to human-readable error descriptions
# - Canonical source of truth for ALL exit code mappings
# - Used by both api.func (telemetry) and error_handler.func (error display)
# - Supports:
#   * Generic/Shell errors (1-3, 10, 124-132, 134, 137, 139, 141, 143-146)
#   * curl/wget errors (4-8, 16, 18, 22-28, 30, 32-36, 39, 44-48, 51-52, 55-57, 59, 61, 63, 75, 78-79, 92, 95)
#   * Package manager errors (APT, DPKG: 100-102, 255)
#   * Script Validation & Setup (103-123)
#   * BSD sysexits (64-78)
#   * Systemd/Service errors (150-154)
#   * Python/pip/uv errors (160-162)
#   * PostgreSQL errors (170-173)
#   * MySQL/MariaDB errors (180-183)
#   * MongoDB errors (190-193)
#   * Proxmox custom codes (200-231)
#   * Tools & Addon Scripts (232-238)
#   * Node.js/npm errors (239, 243, 245-249)
#   * Application Install/Update errors (250-254)
# - Returns description string for given exit code
# ------------------------------------------------------------------------------
explain_exit_code() {
  local code="$1"
  case "$code" in
  # --- Generic / Shell ---
  1) echo "General error / Operation not permitted" ;;
  2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
  3) echo "General syntax or argument error" ;;
  10) echo "Docker / privileged mode required (unsupported environment)" ;;

  # --- curl / wget errors (commonly seen in downloads) ---
  4) echo "curl: Feature not supported or protocol error" ;;
  5) echo "curl: Could not resolve proxy" ;;
  6) echo "curl: DNS resolution failed (could not resolve host)" ;;
  7) echo "curl: Failed to connect (network unreachable / host down)" ;;
  8) echo "curl: Server reply error (FTP/SFTP or apk untrusted key)" ;;
  16) echo "curl: HTTP/2 framing layer error" ;;
  18) echo "curl: Partial file (transfer not completed)" ;;
  22) echo "curl: HTTP error returned (404, 429, 500+)" ;;
  23) echo "curl: Write error (disk full or permissions)" ;;
  24) echo "curl: Write to local file failed" ;;
  25) echo "curl: Upload failed" ;;
  26) echo "curl: Read error on local file (I/O)" ;;
  27) echo "curl: Out of memory (memory allocation failed)" ;;
  28) echo "curl: Operation timeout (network slow or server not responding)" ;;
  30) echo "curl: FTP port command failed" ;;
  32) echo "curl: FTP SIZE command failed" ;;
  33) echo "curl: HTTP range error" ;;
  34) echo "curl: HTTP post error" ;;
  35) echo "curl: SSL/TLS handshake failed (certificate error)" ;;
  36) echo "curl: FTP bad download resume" ;;
  39) echo "curl: LDAP search failed" ;;
  44) echo "curl: Internal error (bad function call order)" ;;
  45) echo "curl: Interface error (failed to bind to specified interface)" ;;
  46) echo "curl: Bad password entered" ;;
  47) echo "curl: Too many redirects" ;;
  48) echo "curl: Unknown command line option specified" ;;
  51) echo "curl: SSL peer certificate or SSH host key verification failed" ;;
  52) echo "curl: Empty reply from server (got nothing)" ;;
  55) echo "curl: Failed sending network data" ;;
  56) echo "curl: Receive error (connection reset by peer)" ;;
  57) echo "curl: Unrecoverable poll/select error (system I/O failure)" ;;
  59) echo "curl: Couldn't use specified SSL cipher" ;;
  61) echo "curl: Bad/unrecognized transfer encoding" ;;
  63) echo "curl: Maximum file size exceeded" ;;
  75) echo "Temporary failure (retry later)" ;;
  78) echo "curl: Remote file not found (404 on FTP/file)" ;;
  79) echo "curl: SSH session error (key exchange/auth failed)" ;;
  92) echo "curl: HTTP/2 stream error (protocol violation)" ;;
  95) echo "curl: HTTP/3 layer error" ;;

  # --- Package manager / APT / DPKG ---
  100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
  101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
  102) echo "APT: Lock held by another process (dpkg/apt still running)" ;;

  # --- Script Validation & Setup (103-123) ---
  103) echo "Validation: Shell is not Bash" ;;
  104) echo "Validation: Not running as root (or invoked via sudo)" ;;
  105) echo "Validation: Proxmox VE version not supported" ;;
  106) echo "Validation: Architecture not supported (ARM / PiMox)" ;;
  107) echo "Validation: Kernel key parameters unreadable" ;;
  108) echo "Validation: Kernel key limits exceeded" ;;
  109) echo "Proxmox: No available container ID after max attempts" ;;
  110) echo "Proxmox: Failed to apply default.vars" ;;
  111) echo "Proxmox: App defaults file not available" ;;
  112) echo "Proxmox: Invalid install menu option" ;;
  113) echo "LXC: Under-provisioned — user aborted update" ;;
  114) echo "LXC: Storage too low — user aborted update" ;;
  115) echo "Download: install.func download failed or incomplete" ;;
  116) echo "Proxmox: Default bridge vmbr0 not found" ;;
  117) echo "LXC: Container did not reach running state" ;;
  118) echo "LXC: No IP assigned to container after timeout" ;;
  119) echo "Proxmox: No valid storage for rootdir content" ;;
  120) echo "Proxmox: No valid storage for vztmpl content" ;;
  121) echo "LXC: Container network not ready (no IP after retries)" ;;
  122) echo "LXC: No internet connectivity — user declined to continue" ;;
  123) echo "LXC: Local IP detection failed" ;;

  # --- BSD sysexits.h (64-78) ---
  64) echo "Usage error (wrong arguments)" ;;
  65) echo "Data format error (bad input data)" ;;
  66) echo "Input file not found (cannot open input)" ;;
  67) echo "User not found (addressee unknown)" ;;
  68) echo "Host not found (hostname unknown)" ;;
  69) echo "Service unavailable" ;;
  70) echo "Internal software error" ;;
  71) echo "System error (OS-level failure)" ;;
  72) echo "Critical OS file missing" ;;
  73) echo "Cannot create output file" ;;
  74) echo "I/O error" ;;
  76) echo "Remote protocol error" ;;
  77) echo "Permission denied" ;;

  # --- Common shell/system errors ---
  124) echo "Command timed out (timeout command)" ;;
  125) echo "Command failed to start (Docker daemon or execution error)" ;;
  126) echo "Command invoked cannot execute (permission problem?)" ;;
  127) echo "Command not found" ;;
  128) echo "Invalid argument to exit" ;;
  129) echo "Killed by SIGHUP (terminal closed / hangup)" ;;
  130) echo "Aborted by user (SIGINT)" ;;
  131) echo "Killed by SIGQUIT (core dumped)" ;;
  132) echo "Killed by SIGILL (illegal CPU instruction)" ;;
  134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;;
  137) echo "Killed (SIGKILL / Out of memory?)" ;;
  139) echo "Segmentation fault (core dumped)" ;;
  141) echo "Broken pipe (SIGPIPE - output closed prematurely)" ;;
  143) echo "Terminated (SIGTERM)" ;;
  144) echo "Killed by signal 16 (SIGUSR1 / SIGSTKFLT)" ;;
  146) echo "Killed by signal 18 (SIGTSTP)" ;;

  # --- Systemd / Service errors (150-154) ---
  150) echo "Systemd: Service failed to start" ;;
  151) echo "Systemd: Service unit not found" ;;
  152) echo "Permission denied (EACCES)" ;;
  153) echo "Build/compile failed (make/gcc/cmake)" ;;
  154) echo "Node.js: Native addon build failed (node-gyp)" ;;
  # --- Python / pip / uv (160-162) ---
  160) echo "Python: Virtualenv / uv environment missing or broken" ;;
  161) echo "Python: Dependency resolution failed" ;;
  162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;

  # --- PostgreSQL (170-173) ---
  170) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
  171) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
  172) echo "PostgreSQL: Database does not exist" ;;
  173) echo "PostgreSQL: Fatal error in query / syntax" ;;

  # --- MySQL / MariaDB (180-183) ---
  180) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
  181) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
  182) echo "MySQL/MariaDB: Database does not exist" ;;
  183) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;

  # --- MongoDB (190-193) ---
  190) echo "MongoDB: Connection failed (server not running)" ;;
  191) echo "MongoDB: Authentication failed (bad user/password)" ;;
  192) echo "MongoDB: Database not found" ;;
  193) echo "MongoDB: Fatal query error" ;;

  # --- Proxmox Custom Codes (200-231) ---
  200) echo "Proxmox: Failed to create lock file" ;;
  203) echo "Proxmox: Missing CTID variable" ;;
  204) echo "Proxmox: Missing PCT_OSTYPE variable" ;;
  205) echo "Proxmox: Invalid CTID (<100)" ;;
  206) echo "Proxmox: CTID already in use" ;;
  207) echo "Proxmox: Password contains unescaped special characters" ;;
  208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;;
  209) echo "Proxmox: Container creation failed" ;;
  210) echo "Proxmox: Cluster not quorate" ;;
  211) echo "Proxmox: Timeout waiting for template lock" ;;
  212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;;
  213) echo "Proxmox: Storage type does not support 'rootdir' content" ;;
  214) echo "Proxmox: Not enough storage space" ;;
  215) echo "Proxmox: Container created but not listed (ghost state)" ;;
  216) echo "Proxmox: RootFS entry missing in config" ;;
  217) echo "Proxmox: Storage not accessible" ;;
  218) echo "Proxmox: Template file corrupted or incomplete" ;;
  219) echo "Proxmox: CephFS does not support containers - use RBD" ;;
  220) echo "Proxmox: Unable to resolve template path" ;;
  221) echo "Proxmox: Template file not readable" ;;
  222) echo "Proxmox: Template download failed" ;;
  223) echo "Proxmox: Template not available after download" ;;
  224) echo "Proxmox: PBS storage is for backups only" ;;
  225) echo "Proxmox: No template available for OS/Version" ;;
  226) echo "Proxmox: VM disk import or post-creation setup failed" ;;
  231) echo "Proxmox: LXC stack upgrade failed" ;;

  # --- Tools & Addon Scripts (232-238) ---
  232) echo "Tools: Wrong execution environment (run on PVE host, not inside LXC)" ;;
  233) echo "Tools: Application not installed (update prerequisite missing)" ;;
  234) echo "Tools: No LXC containers found or available" ;;
  235) echo "Tools: Backup or restore operation failed" ;;
  236) echo "Tools: Required hardware not detected" ;;
  237) echo "Tools: Dependency package installation failed" ;;
  238) echo "Tools: OS or distribution not supported for this addon" ;;

  # --- Node.js / npm / pnpm / yarn (239-249) ---
  239) echo "npm/Node.js: Unexpected runtime error or dependency failure" ;;
  243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
  245) echo "Node.js: Invalid command-line option" ;;
  246) echo "Node.js: Internal JavaScript Parse Error" ;;
  247) echo "Node.js: Fatal internal error" ;;
  248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
  249) echo "npm/pnpm/yarn: Unknown fatal error" ;;

  # --- Application Install/Update Errors (250-254) ---
  250) echo "App: Download failed or version not determined" ;;
  251) echo "App: File extraction failed (corrupt or incomplete archive)" ;;
  252) echo "App: Required file or resource not found" ;;
  253) echo "App: Data migration required — update aborted" ;;
  254) echo "App: User declined prompt or input timed out" ;;

  # --- DPKG ---
  255) echo "DPKG: Fatal internal error" ;;

  # --- Default ---
  *) echo "Unknown error" ;;
  esac
}

# ------------------------------------------------------------------------------
# json_escape()
#
# - Escapes a string for safe JSON embedding
# - Strips ANSI escape sequences and non-printable control characters
# - Handles backslashes, quotes, newlines, tabs, and carriage returns
# ------------------------------------------------------------------------------
json_escape() {
  # Escape a string for safe JSON embedding using awk (handles any input size).
  # Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n
  printf '%s' "$1" \
    | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
    | tr -d '\000-\010\013\014\016-\037\177\r' \
    | awk '
        BEGIN { ORS = "" }
        {
          gsub(/\\/, "\\\\")   # backslash → \\
          gsub(/"/, "\\\"")    # double quote → \"
          gsub(/\t/, "\\t")   # tab → \t
          if (NR > 1) printf "\\n"
          printf "%s", $0
        }'
}

# ------------------------------------------------------------------------------
# get_error_text()
#
# - Returns last 20 lines of the active log (INSTALL_LOG or BUILD_LOG)
# - Falls back to combined log or BUILD_LOG if primary is not accessible
# - Handles container paths that don't exist on the host
# ------------------------------------------------------------------------------
get_error_text() {
  local logfile=""
  if declare -f get_active_logfile >/dev/null 2>&1; then
    logfile=$(get_active_logfile)
  elif [[ -n "${INSTALL_LOG:-}" ]]; then
    logfile="$INSTALL_LOG"
  elif [[ -n "${BUILD_LOG:-}" ]]; then
    logfile="$BUILD_LOG"
  fi

  # If logfile is inside container (e.g. /root/.install-*), try the host copy
  if [[ -n "$logfile" && ! -s "$logfile" ]]; then
    # Try combined log: /tmp/<app>-<CTID>-<SESSION_ID>.log
    if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then
      local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
      if [[ -s "$combined_log" ]]; then
        logfile="$combined_log"
      fi
    fi
  fi

  # Also try BUILD_LOG as fallback if primary log is empty/missing
  if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
    logfile="$BUILD_LOG"
  fi

  # Try SILENT_LOGFILE as last resort (captures $STD command output)
  if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
    logfile="$SILENT_LOGFILE"
  fi

  if [[ -n "$logfile" && -s "$logfile" ]]; then
    tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
  fi
}

# ------------------------------------------------------------------------------
# get_full_log()
#
# - Returns the FULL installation log (build + install combined)
# - Calls ensure_log_on_host() to pull container log if needed
# - Strips ANSI escape codes and carriage returns
# - Truncates to max_bytes (default: 120KB) to stay within API limits
# - Used for the error telemetry field (full trace instead of 20 lines)
# ------------------------------------------------------------------------------
get_full_log() {
  local max_bytes="${1:-122880}" # 120KB default
  local logfile=""

  # Ensure logs are available on host (pulls from container if needed)
  if declare -f ensure_log_on_host >/dev/null 2>&1; then
    ensure_log_on_host
  fi

  # Try combined log first (most complete)
  if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then
    local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
    if [[ -s "$combined_log" ]]; then
      logfile="$combined_log"
    fi
  fi

  # Fall back to INSTALL_LOG
  if [[ -z "$logfile" || ! -s "$logfile" ]]; then
    if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then
      logfile="$INSTALL_LOG"
    fi
  fi

  # Fall back to BUILD_LOG
  if [[ -z "$logfile" || ! -s "$logfile" ]]; then
    if [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
      logfile="$BUILD_LOG"
    fi
  fi

  # Fall back to SILENT_LOGFILE (captures $STD command output)
  if [[ -z "$logfile" || ! -s "$logfile" ]]; then
    if [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
      logfile="$SILENT_LOGFILE"
    fi
  fi

  if [[ -n "$logfile" && -s "$logfile" ]]; then
    # Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR)
    sed 's/\r$//' "$logfile" 2>/dev/null |
      sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' |
      sed -E 's/([0-9]{1,3}\.)[0-9]{1,3}\.[0-9]{1,3}/\1x.x/g' |
      head -c "$max_bytes"
  fi
}

# ------------------------------------------------------------------------------
# build_error_string()
#
# - Builds a structured error string for telemetry reporting
# - Format: "exit_code=<N> | <explanation>\n---\n<last 20 log lines>"
# - If no log lines available, returns just the explanation
# - Arguments:
#   * $1: exit_code (numeric)
#   * $2: log_text (optional, output from get_error_text)
# - Returns structured error string via stdout
# ------------------------------------------------------------------------------
build_error_string() {
  local exit_code="${1:-1}"
  local log_text="${2:-}"
  local explanation
  explanation=$(explain_exit_code "$exit_code")

  if [[ -n "$log_text" ]]; then
    # Structured format: header + separator + log lines
    printf 'exit_code=%s | %s\n---\n%s' "$exit_code" "$explanation" "$log_text"
  else
    # No log available - just the explanation with exit code
    printf 'exit_code=%s | %s' "$exit_code" "$explanation"
  fi
}

# ==============================================================================
# SECTION 2: TELEMETRY FUNCTIONS
# ==============================================================================

# ------------------------------------------------------------------------------
# detect_gpu()
#
# - Detects GPU vendor, model, and passthrough type
# - Sets GPU_VENDOR, GPU_MODEL, and GPU_PASSTHROUGH globals
# - Used for GPU analytics
# ------------------------------------------------------------------------------
detect_gpu() {
  GPU_VENDOR="unknown"
  GPU_MODEL=""
  GPU_PASSTHROUGH="unknown"

  local gpu_line
  gpu_line=$(lspci 2>/dev/null | grep -iE "VGA|3D|Display" | head -1)

  if [[ -n "$gpu_line" ]]; then
    # Extract model: everything after the colon, clean up
    GPU_MODEL=$(echo "$gpu_line" | sed 's/.*: //' | sed 's/ (rev .*)$//' | cut -c1-64)

    # Detect vendor and passthrough type
    if echo "$gpu_line" | grep -qi "Intel"; then
      GPU_VENDOR="intel"
      GPU_PASSTHROUGH="igpu"
    elif echo "$gpu_line" | grep -qi "AMD\|ATI"; then
      GPU_VENDOR="amd"
      if echo "$gpu_line" | grep -qi "Radeon RX\|Radeon Pro"; then
        GPU_PASSTHROUGH="dgpu"
      else
        GPU_PASSTHROUGH="igpu"
      fi
    elif echo "$gpu_line" | grep -qi "NVIDIA"; then
      GPU_VENDOR="nvidia"
      GPU_PASSTHROUGH="dgpu"
    fi
  fi

  export GPU_VENDOR GPU_MODEL GPU_PASSTHROUGH
}

# ------------------------------------------------------------------------------
# detect_cpu()
#
# - Detects CPU vendor and model
# - Sets CPU_VENDOR (intel/amd/arm/unknown) and CPU_MODEL globals
# - Used for CPU analytics
# ------------------------------------------------------------------------------
detect_cpu() {
  CPU_VENDOR="unknown"
  CPU_MODEL=""

  if [[ -f /proc/cpuinfo ]]; then
    local vendor_id
    vendor_id=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | tr -d ' ')

    case "$vendor_id" in
    GenuineIntel) CPU_VENDOR="intel" ;;
    AuthenticAMD) CPU_VENDOR="amd" ;;
    *)
      # ARM doesn't have vendor_id, check for CPU implementer
      if grep -qi "CPU implementer" /proc/cpuinfo 2>/dev/null; then
        CPU_VENDOR="arm"
      fi
      ;;
    esac

    # Extract model name and clean it up
    CPU_MODEL=$(grep -m1 "model name" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ *//' | sed 's/(R)//g' | sed 's/(TM)//g' | sed 's/  */ /g' | cut -c1-64)
  fi

  export CPU_VENDOR CPU_MODEL
}

# ------------------------------------------------------------------------------
# detect_ram()
#
# - Detects RAM speed using dmidecode
# - Sets RAM_SPEED global (e.g., "4800" for DDR5-4800)
# - Requires root access for dmidecode
# - Returns empty if not available or if speed is "Unknown" (nested VMs)
# ------------------------------------------------------------------------------
detect_ram() {
  RAM_SPEED=""

  if command -v dmidecode &>/dev/null; then
    # Get configured memory speed (actual running speed)
    # Use || true to handle "Unknown" values in nested VMs (no numeric match)
    RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) || true

    # Fallback to Speed: if Configured not available
    if [[ -z "$RAM_SPEED" ]]; then
      RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) || true
    fi
  fi

  export RAM_SPEED
}

# ------------------------------------------------------------------------------
# post_to_api()
#
# - Sends LXC container creation statistics to telemetry ingest service
# - Only executes if:
#   * curl is available
#   * DIAGNOSTICS=yes
#   * RANDOM_UUID is set
# - Payload includes:
#   * Container type, disk size, CPU cores, RAM
#   * OS type and version
#   * Application name (NSAPP)
#   * Installation method
#   * PVE version
#   * Status: "installing"
#   * Random UUID for session tracking
# - Anonymous telemetry (no personal data)
# - Never blocks or fails script execution
# ------------------------------------------------------------------------------
post_to_api() {
  # Prevent duplicate submissions (post_to_api is called from multiple places)
  [[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0

  # Silent fail - telemetry should never break scripts
  command -v curl &>/dev/null || {
    [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] curl not found, skipping" >&2
    return 0
  }
  [[ "${DIAGNOSTICS:-no}" == "no" ]] && {
    [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] DIAGNOSTICS=no, skipping" >&2
    return 0
  }
  [[ -z "${RANDOM_UUID:-}" ]] && {
    [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] RANDOM_UUID empty, skipping" >&2
    return 0
  }

  [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] post_to_api() DIAGNOSTICS=$DIAGNOSTICS RANDOM_UUID=$RANDOM_UUID NSAPP=$NSAPP" >&2

  # Set type for later status updates
  TELEMETRY_TYPE="lxc"

  local pve_version=""
  if command -v pveversion &>/dev/null; then
    pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
  fi

  # Detect GPU if not already set
  if [[ -z "${GPU_VENDOR:-}" ]]; then
    detect_gpu
  fi
  local gpu_vendor="${GPU_VENDOR:-unknown}"
  local gpu_model
  gpu_model=$(json_escape "${GPU_MODEL:-}")
  local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"

  # Detect CPU if not already set
  if [[ -z "${CPU_VENDOR:-}" ]]; then
    detect_cpu
  fi
  local cpu_vendor="${CPU_VENDOR:-unknown}"
  local cpu_model
  cpu_model=$(json_escape "${CPU_MODEL:-}")

  # Detect RAM if not already set
  if [[ -z "${RAM_SPEED:-}" ]]; then
    detect_ram
  fi
  local ram_speed="${RAM_SPEED:-}"

  local JSON_PAYLOAD
  JSON_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${RANDOM_UUID}",
    "execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
    "type": "lxc",
    "nsapp": "${NSAPP:-unknown}",
    "status": "installing",
    "ct_type": ${CT_TYPE:-1},
    "disk_size": ${DISK_SIZE:-0},
    "core_count": ${CORE_COUNT:-0},
    "ram_size": ${RAM_SIZE:-0},
    "os_type": "${var_os:-}",
    "os_version": "${var_version:-}",
    "pve_version": "${pve_version}",
    "method": "${METHOD:-default}",
    "cpu_vendor": "${cpu_vendor}",
    "cpu_model": "${cpu_model}",
    "gpu_vendor": "${gpu_vendor}",
    "gpu_model": "${gpu_model}",
    "gpu_passthrough": "${gpu_passthrough}",
    "ram_speed": "${ram_speed}",
    "repo_source": "${REPO_SOURCE}"
}
EOF
  )

  [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2
  [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2

  # Send initial "installing" record with retry.
  # This record MUST exist for all subsequent updates to succeed.
  local http_code="" attempt
  for attempt in 1 2 3; do
    if [[ "${DEV_MODE:-}" == "true" ]]; then
      http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
        -H "Content-Type: application/json" \
        -d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || http_code="000"
      echo "[DEBUG] post_to_api attempt $attempt HTTP=$http_code" >&2
    else
      http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
        -H "Content-Type: application/json" \
        -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
    fi
    [[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
    [[ "$attempt" -lt 3 ]] && sleep 1
  done

  POST_TO_API_DONE=true
}

# ------------------------------------------------------------------------------
# post_to_api_vm()
#
# - Sends VM creation statistics to telemetry ingest service
# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics file
# - Payload differences from LXC:
#   * ct_type=2 (VM instead of LXC)
#   * type="vm"
#   * Disk size without 'G' suffix
# - Includes hardware detection: CPU, GPU, RAM speed
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
# - Never blocks or fails script execution
# ------------------------------------------------------------------------------
post_to_api_vm() {
  # Read diagnostics setting from file
  if [[ -f /usr/local/community-scripts/diagnostics ]]; then
    DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true
  fi

  # Silent fail - telemetry should never break scripts
  command -v curl &>/dev/null || return 0
  [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
  [[ -z "${RANDOM_UUID:-}" ]] && return 0

  # Set type for later status updates
  TELEMETRY_TYPE="vm"

  local pve_version=""
  if command -v pveversion &>/dev/null; then
    pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
  fi

  # Detect GPU if not already set
  if [[ -z "${GPU_VENDOR:-}" ]]; then
    detect_gpu
  fi
  local gpu_vendor="${GPU_VENDOR:-unknown}"
  local gpu_model
  gpu_model=$(json_escape "${GPU_MODEL:-}")
  local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"

  # Detect CPU if not already set
  if [[ -z "${CPU_VENDOR:-}" ]]; then
    detect_cpu
  fi
  local cpu_vendor="${CPU_VENDOR:-unknown}"
  local cpu_model
  cpu_model=$(json_escape "${CPU_MODEL:-}")

  # Detect RAM if not already set
  if [[ -z "${RAM_SPEED:-}" ]]; then
    detect_ram
  fi
  local ram_speed="${RAM_SPEED:-}"

  # Remove 'G' suffix from disk size
  local DISK_SIZE_API="${DISK_SIZE%G}"

  local JSON_PAYLOAD
  JSON_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${RANDOM_UUID}",
    "execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
    "type": "vm",
    "nsapp": "${NSAPP:-unknown}",
    "status": "installing",
    "ct_type": 2,
    "disk_size": ${DISK_SIZE_API:-0},
    "core_count": ${CORE_COUNT:-0},
    "ram_size": ${RAM_SIZE:-0},
    "os_type": "${var_os:-}",
    "os_version": "${var_version:-}",
    "pve_version": "${pve_version}",
    "method": "${METHOD:-default}",
    "cpu_vendor": "${cpu_vendor}",
    "cpu_model": "${cpu_model}",
    "gpu_vendor": "${gpu_vendor}",
    "gpu_model": "${gpu_model}",
    "gpu_passthrough": "${gpu_passthrough}",
    "ram_speed": "${ram_speed}",
    "repo_source": "${REPO_SOURCE}"
}
EOF
  )

  # Send initial "installing" record with retry (must succeed for updates to work)
  local http_code="" attempt
  for attempt in 1 2 3; do
    http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
      -H "Content-Type: application/json" \
      -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
    [[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
    [[ "$attempt" -lt 3 ]] && sleep 1
  done

  POST_TO_API_DONE=true
}

# ------------------------------------------------------------------------------
# post_progress_to_api()
#
# - Lightweight progress ping from host or container
# - Updates the existing telemetry record status
# - Arguments:
#   * $1: status (optional, default: "configuring")
#         Valid values: "validation", "configuring"
# - Signals that the installation is actively progressing (not stuck)
# - Fire-and-forget: never blocks or fails the script
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
# - Can be called multiple times safely
# ------------------------------------------------------------------------------
post_progress_to_api() {
  command -v curl &>/dev/null || return 0
  [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
  [[ -z "${RANDOM_UUID:-}" ]] && return 0

  local progress_status="${1:-configuring}"
  local app_name="${NSAPP:-${app:-unknown}}"
  local telemetry_type="${TELEMETRY_TYPE:-lxc}"

  curl -fsS -m 5 -X POST "${TELEMETRY_URL:-https://telemetry.community-scripts.org/telemetry}" \
    -H "Content-Type: application/json" \
    -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${telemetry_type}\",\"nsapp\":\"${app_name}\",\"status\":\"${progress_status}\"}" &>/dev/null || true
}

# ------------------------------------------------------------------------------
# post_update_to_api()
#
# - Reports installation completion status to telemetry ingest service
# - Prevents duplicate submissions via POST_UPDATE_DONE flag
# - Arguments:
#   * $1: status ("done" or "failed")
#   * $2: exit_code (numeric, default: 1 for failed, 0 for done)
# - Payload includes:
#   * Final status (mapped: "done"→"success", "failed"→"failed")
#   * Error description via explain_exit_code()
#   * Numeric exit code
# - Only executes once per session
# - Never blocks or fails script execution
# ------------------------------------------------------------------------------
post_update_to_api() {
  # Silent fail - telemetry should never break scripts
  command -v curl &>/dev/null || return 0

  # Support "force" mode (3rd arg) to bypass duplicate check for retries after cleanup
  local force="${3:-}"
  POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
  if [[ "$POST_UPDATE_DONE" == "true" && "$force" != "force" ]]; then
    return 0
  fi

  [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
  [[ -z "${RANDOM_UUID:-}" ]] && return 0

  local status="${1:-failed}"
  local raw_exit_code="${2:-1}"
  local exit_code=0 error="" pb_status error_category=""

  # Get GPU info (if detected)
  local gpu_vendor="${GPU_VENDOR:-unknown}"
  local gpu_model
  gpu_model=$(json_escape "${GPU_MODEL:-}")
  local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"

  # Get CPU info (if detected)
  local cpu_vendor="${CPU_VENDOR:-unknown}"
  local cpu_model
  cpu_model=$(json_escape "${CPU_MODEL:-}")

  # Get RAM info (if detected)
  local ram_speed="${RAM_SPEED:-}"

  # Map status to telemetry values: installing, success, failed, unknown
  case "$status" in
  done | success)
    pb_status="success"
    exit_code=0
    error=""
    error_category=""
    ;;
  failed)
    pb_status="failed"
    ;;
  *)
    pb_status="unknown"
    ;;
  esac

  # For failed/unknown status, resolve exit code and error description
  local short_error="" medium_error=""
  if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then
    if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then
      exit_code="$raw_exit_code"
    else
      exit_code=1
    fi
    # Get full installation log for error field
    local log_text=""
    log_text=$(get_full_log 122880) || true # 120KB max
    if [[ -z "$log_text" ]]; then
      # Fallback to last 20 lines
      log_text=$(get_error_text)
    fi
    local full_error
    full_error=$(build_error_string "$exit_code" "$log_text")
    error=$(json_escape "$full_error")
    short_error=$(json_escape "$(explain_exit_code "$exit_code")")
    error_category=$(categorize_error "$exit_code")
    [[ -z "$error" ]] && error="Unknown error"

    # Build medium error for attempt 2: explanation + last 100 log lines (≤16KB)
    # This is the critical middle ground between full 120KB log and generic-only description
    local medium_log=""
    medium_log=$(get_full_log 16384) || true # 16KB max
    if [[ -z "$medium_log" ]]; then
      medium_log=$(get_error_text) || true
    fi
    local medium_full
    medium_full=$(build_error_string "$exit_code" "$medium_log")
    medium_error=$(json_escape "$medium_full")
    [[ -z "$medium_error" ]] && medium_error="$short_error"
  fi

  # Calculate duration if timer was started
  local duration=0
  if [[ -n "${INSTALL_START_TIME:-}" ]]; then
    duration=$(($(date +%s) - INSTALL_START_TIME))
  fi

  # Get PVE version
  local pve_version=""
  if command -v pveversion &>/dev/null; then
    pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
  fi

  local http_code=""

  # Strip 'G' suffix from disk size (VMs set DISK_SIZE=32G)
  local DISK_SIZE_API="${DISK_SIZE:-0}"
  DISK_SIZE_API="${DISK_SIZE_API%G}"
  [[ ! "$DISK_SIZE_API" =~ ^[0-9]+$ ]] && DISK_SIZE_API=0

  # ── Attempt 1: Full payload with complete error text (includes full log) ──
  local JSON_PAYLOAD
  JSON_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${RANDOM_UUID}",
    "execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
    "type": "${TELEMETRY_TYPE:-lxc}",
    "nsapp": "${NSAPP:-unknown}",
    "status": "${pb_status}",
    "ct_type": ${CT_TYPE:-1},
    "disk_size": ${DISK_SIZE_API},
    "core_count": ${CORE_COUNT:-0},
    "ram_size": ${RAM_SIZE:-0},
    "os_type": "${var_os:-}",
    "os_version": "${var_version:-}",
    "pve_version": "${pve_version}",
    "method": "${METHOD:-default}",
    "exit_code": ${exit_code},
    "error": "${error}",
    "error_category": "${error_category}",
    "install_duration": ${duration},
    "cpu_vendor": "${cpu_vendor}",
    "cpu_model": "${cpu_model}",
    "gpu_vendor": "${gpu_vendor}",
    "gpu_model": "${gpu_model}",
    "gpu_passthrough": "${gpu_passthrough}",
    "ram_speed": "${ram_speed}",
    "repo_source": "${REPO_SOURCE}"
}
EOF
  )

  http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
    -H "Content-Type: application/json" \
    -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"

  if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
    POST_UPDATE_DONE=true
    return 0
  fi

  # ── Attempt 2: Medium error text (truncated log ≤16KB instead of full 120KB) ──
  sleep 1
  local RETRY_PAYLOAD
  RETRY_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${RANDOM_UUID}",
    "execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
    "type": "${TELEMETRY_TYPE:-lxc}",
    "nsapp": "${NSAPP:-unknown}",
    "status": "${pb_status}",
    "ct_type": ${CT_TYPE:-1},
    "disk_size": ${DISK_SIZE_API},
    "core_count": ${CORE_COUNT:-0},
    "ram_size": ${RAM_SIZE:-0},
    "os_type": "${var_os:-}",
    "os_version": "${var_version:-}",
    "pve_version": "${pve_version}",
    "method": "${METHOD:-default}",
    "exit_code": ${exit_code},
    "error": "${medium_error}",
    "error_category": "${error_category}",
    "install_duration": ${duration},
    "cpu_vendor": "${cpu_vendor}",
    "cpu_model": "${cpu_model}",
    "gpu_vendor": "${gpu_vendor}",
    "gpu_model": "${gpu_model}",
    "gpu_passthrough": "${gpu_passthrough}",
    "ram_speed": "${ram_speed}",
    "repo_source": "${REPO_SOURCE}"
}
EOF
  )

  http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
    -H "Content-Type: application/json" \
    -d "$RETRY_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"

  if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
    POST_UPDATE_DONE=true
    return 0
  fi

  # ── Attempt 3: Minimal payload with medium error (bare minimum to set status) ──
  sleep 2
  local MINIMAL_PAYLOAD
  MINIMAL_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${RANDOM_UUID}",
    "execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
    "type": "${TELEMETRY_TYPE:-lxc}",
    "nsapp": "${NSAPP:-unknown}",
    "status": "${pb_status}",
    "exit_code": ${exit_code},
    "error": "${medium_error}",
    "error_category": "${error_category}",
    "install_duration": ${duration}
}
EOF
  )

  http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
    -H "Content-Type: application/json" \
    -d "$MINIMAL_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"

  if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
    POST_UPDATE_DONE=true
    return 0
  fi

  # All 3 attempts failed — do NOT set POST_UPDATE_DONE=true.
  # This allows the EXIT trap (on_exit in error_handler.func) to retry.
  # No infinite loop risk: EXIT trap fires exactly once.
}

# ==============================================================================
# SECTION 3: EXTENDED TELEMETRY FUNCTIONS
# ==============================================================================

# ------------------------------------------------------------------------------
# categorize_error()
#
# - Maps exit codes to error categories for better analytics
# - Categories: network, storage, dependency, permission, timeout, config, resource, unknown
# - Used to group errors in dashboard
# ------------------------------------------------------------------------------
categorize_error() {
  local code="$1"
  case "$code" in
  # Network errors (curl/wget)
  6 | 7 | 22 | 35) echo "network" ;;

  # Docker / Privileged mode required
  10) echo "config" ;;

  # Timeout errors
  28 | 124 | 211) echo "timeout" ;;

  # Storage errors (Proxmox storage)
  214 | 217 | 219 | 224) echo "storage" ;;

  # Dependency/Package errors (APT, DPKG, pip, commands)
  100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;;

  # Permission errors
  126 | 152) echo "permission" ;;

  # Configuration errors (Proxmox config, invalid args)
  128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;;

  # Proxmox container/template errors
  200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;;

  # Service/Systemd errors
  150 | 151 | 153 | 154) echo "service" ;;

  # Database errors (PostgreSQL, MySQL, MongoDB)
  170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;;

  # Node.js / JavaScript runtime errors
  243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;;

  # Python environment errors
  # (already covered: 160-162 under dependency)

  # Aborted by user (SIGHUP=terminal closed, SIGINT=Ctrl+C, SIGTERM=killed)
  129 | 130 | 143) echo "user_aborted" ;;

  # Resource errors (OOM, SIGKILL, SIGABRT)
  134 | 137) echo "resource" ;;

  # Signal/Process errors (SIGPIPE, SIGSEGV)
  139 | 141) echo "signal" ;;

  # Shell errors (general error, syntax error)
  1 | 2) echo "shell" ;;

  # Default - truly unknown
  *) echo "unknown" ;;
  esac
}

# ------------------------------------------------------------------------------
# start_install_timer()
#
# - Captures start time for installation duration tracking
# - Call at the beginning of installation
# - Sets INSTALL_START_TIME global variable
# ------------------------------------------------------------------------------
start_install_timer() {
  INSTALL_START_TIME=$(date +%s)
  export INSTALL_START_TIME
}

# ------------------------------------------------------------------------------
# get_install_duration()
#
# - Returns elapsed seconds since start_install_timer() was called
# - Returns 0 if timer was not started
# ------------------------------------------------------------------------------
get_install_duration() {
  if [[ -z "${INSTALL_START_TIME:-}" ]]; then
    echo "0"
    return
  fi
  local now=$(date +%s)
  echo $((now - INSTALL_START_TIME))
}

# ------------------------------------------------------------------------------
# _telemetry_report_exit()
#
# - Internal handler called by EXIT trap set in init_tool_telemetry()
# - Determines success/failure from exit code and reports via appropriate API
# - Arguments:
#   * $1: exit_code from the script
# ------------------------------------------------------------------------------
_telemetry_report_exit() {
  local ec="${1:-0}"
  local status="success"
  [[ "$ec" -ne 0 ]] && status="failed"

  # Lazy name resolution: use explicit name, fall back to $APP, then "unknown"
  local name="${TELEMETRY_TOOL_NAME:-${APP:-unknown}}"

  if [[ "${TELEMETRY_TOOL_TYPE:-pve}" == "addon" ]]; then
    post_addon_to_api "$name" "$status" "$ec"
  else
    post_tool_to_api "$name" "$status" "$ec"
  fi
}

# ------------------------------------------------------------------------------
# init_tool_telemetry()
#
# - One-line telemetry setup for tools/addon scripts
# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics
#   (persisted on PVE host during first build, and inside containers by install.func)
# - Starts install timer for duration tracking
# - Sets EXIT trap to automatically report success/failure on script exit
# - Arguments:
#   * $1: tool_name (optional, falls back to $APP at exit time)
#   * $2: type ("pve" for PVE host scripts, "addon" for container addons)
# - Usage:
#   source <(curl -fsSL .../misc/api.func) 2>/dev/null || true
#   init_tool_telemetry "post-pve-install" "pve"
#   init_tool_telemetry "" "addon"  # uses $APP at exit time
# ------------------------------------------------------------------------------
init_tool_telemetry() {
  local name="${1:-}"
  local type="${2:-pve}"

  [[ -n "$name" ]] && TELEMETRY_TOOL_NAME="$name"
  TELEMETRY_TOOL_TYPE="$type"

  # Read diagnostics opt-in/opt-out
  if [[ -f /usr/local/community-scripts/diagnostics ]]; then
    DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true
  fi

  start_install_timer

  # EXIT trap: automatically report telemetry when script ends
  trap '_telemetry_report_exit "$?"' EXIT
}

# ------------------------------------------------------------------------------
# post_tool_to_api()
#
# - Reports tool usage to telemetry
# - Arguments:
#   * $1: tool_name (e.g., "microcode", "lxc-update", "post-pve-install")
#   * $2: status ("success" or "failed")
#   * $3: exit_code (optional, default: 0 for success, 1 for failed)
# - For PVE host tools, not container installations
# ------------------------------------------------------------------------------
post_tool_to_api() {
  command -v curl &>/dev/null || return 0
  [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0

  local tool_name="${1:-unknown}"
  local status="${2:-success}"
  local exit_code="${3:-0}"
  local error="" error_category=""
  local uuid duration

  # Generate UUID for this tool execution
  uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "tool-$(date +%s)")
  duration=$(get_install_duration)

  # Map status
  [[ "$status" == "done" ]] && status="success"

  if [[ "$status" == "failed" ]]; then
    [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
    local error_text=""
    error_text=$(get_error_text)
    local full_error
    full_error=$(build_error_string "$exit_code" "$error_text")
    error=$(json_escape "$full_error")
    error_category=$(categorize_error "$exit_code")
  fi

  local pve_version=""
  if command -v pveversion &>/dev/null; then
    pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
  fi

  local JSON_PAYLOAD
  JSON_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${uuid}",
    "execution_id": "${EXECUTION_ID:-${uuid}}",
    "type": "pve",
    "nsapp": "${tool_name}",
    "status": "${status}",
    "exit_code": ${exit_code},
    "error": "${error}",
    "error_category": "${error_category}",
    "install_duration": ${duration:-0},
    "pve_version": "${pve_version}",
    "repo_source": "${REPO_SOURCE}"
}
EOF
  )

  curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
    -H "Content-Type: application/json" \
    -d "$JSON_PAYLOAD" &>/dev/null || true
}

# ------------------------------------------------------------------------------
# post_addon_to_api()
#
# - Reports addon installation to telemetry
# - Arguments:
#   * $1: addon_name (e.g., "filebrowser", "netdata")
#   * $2: status ("success" or "failed")
#   * $3: exit_code (optional)
# - For addons installed inside containers
# ------------------------------------------------------------------------------
post_addon_to_api() {
  command -v curl &>/dev/null || return 0
  [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0

  local addon_name="${1:-unknown}"
  local status="${2:-success}"
  local exit_code="${3:-0}"
  local error="" error_category=""
  local uuid duration

  # Generate UUID for this addon installation
  uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "addon-$(date +%s)")
  duration=$(get_install_duration)

  # Map status
  [[ "$status" == "done" ]] && status="success"

  if [[ "$status" == "failed" ]]; then
    [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
    local error_text=""
    error_text=$(get_error_text)
    local full_error
    full_error=$(build_error_string "$exit_code" "$error_text")
    error=$(json_escape "$full_error")
    error_category=$(categorize_error "$exit_code")
  fi

  # Detect OS info
  local os_type="" os_version=""
  if [[ -f /etc/os-release ]]; then
    os_type=$(grep "^ID=" /etc/os-release | cut -d= -f2 | tr -d '"')
    os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d= -f2 | tr -d '"')
  fi

  local JSON_PAYLOAD
  JSON_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${uuid}",
    "execution_id": "${EXECUTION_ID:-${uuid}}",
    "type": "addon",
    "nsapp": "${addon_name}",
    "status": "${status}",
    "exit_code": ${exit_code},
    "error": "${error}",
    "error_category": "${error_category}",
    "install_duration": ${duration:-0},
    "os_type": "${os_type}",
    "os_version": "${os_version}",
    "repo_source": "${REPO_SOURCE}"
}
EOF
  )

  curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
    -H "Content-Type: application/json" \
    -d "$JSON_PAYLOAD" &>/dev/null || true
}

# ------------------------------------------------------------------------------
# post_update_to_api_extended()
#
# - Extended version of post_update_to_api with duration, GPU, and error category
# - Same arguments as post_update_to_api:
#   * $1: status ("done" or "failed")
#   * $2: exit_code (numeric)
# - Automatically includes:
#   * Install duration (if start_install_timer was called)
#   * Error category (for failed status)
#   * GPU info (if detect_gpu was called)
# ------------------------------------------------------------------------------
post_update_to_api_extended() {
  # Silent fail - telemetry should never break scripts
  command -v curl &>/dev/null || return 0

  # Prevent duplicate submissions
  POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
  [[ "$POST_UPDATE_DONE" == "true" ]] && return 0

  [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
  [[ -z "${RANDOM_UUID:-}" ]] && return 0

  local status="${1:-failed}"
  local raw_exit_code="${2:-1}"
  local exit_code=0 error="" pb_status error_category=""
  local duration gpu_vendor gpu_passthrough

  # Get duration
  duration=$(get_install_duration)

  # Get GPU info (if detected)
  gpu_vendor="${GPU_VENDOR:-}"
  gpu_passthrough="${GPU_PASSTHROUGH:-}"

  # Map status to telemetry values
  case "$status" in
  done | success)
    pb_status="success"
    exit_code=0
    error=""
    error_category=""
    ;;
  failed)
    pb_status="failed"
    ;;
  *)
    pb_status="unknown"
    ;;
  esac

  # For failed/unknown status, resolve exit code and error description
  if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then
    if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then
      exit_code="$raw_exit_code"
    else
      exit_code=1
    fi
    local error_text=""
    error_text=$(get_error_text)
    local full_error
    full_error=$(build_error_string "$exit_code" "$error_text")
    error=$(json_escape "$full_error")
    error_category=$(categorize_error "$exit_code")
    [[ -z "$error" ]] && error="Unknown error"
  fi

  local JSON_PAYLOAD
  JSON_PAYLOAD=$(
    cat <<EOF
{
    "random_id": "${RANDOM_UUID}",
    "execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
    "type": "${TELEMETRY_TYPE:-lxc}",
    "nsapp": "${NSAPP:-unknown}",
    "status": "${pb_status}",
    "exit_code": ${exit_code},
    "error": "${error}",
    "error_category": "${error_category}",
    "install_duration": ${duration:-0},
    "gpu_vendor": "${gpu_vendor}",
    "gpu_passthrough": "${gpu_passthrough}",
    "repo_source": "${REPO_SOURCE}"
}
EOF
  )

  local http_code
  http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
    -H "Content-Type: application/json" \
    -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"

  if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
    POST_UPDATE_DONE=true
    return 0
  fi

  # Retry with minimal payload
  sleep 1
  http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
    -H "Content-Type: application/json" \
    -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-unknown}\",\"status\":\"${pb_status}\",\"exit_code\":${exit_code},\"install_duration\":${duration:-0}}" \
    -o /dev/null 2>/dev/null) || http_code="000"

  if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
    POST_UPDATE_DONE=true
    return 0
  fi

  # Do NOT set POST_UPDATE_DONE=true — let EXIT trap retry
}
