#!/bin/bash

# ==========================================================
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author      : MacRimi
# Copyright   : (c) 2024 MacRimi
# License     : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version     : 1.2
# Last Updated: 12/04/2026
# ==========================================================
# Description:
# This script allows users to assign physical disks to existing
# Proxmox virtual machines (VMs) through an interactive menu.
# - Detects the system disk and excludes it from selection.
# - Lists all available VMs for the user to choose from.
# - Identifies and displays unassigned physical disks.
# - Allows the user to select multiple disks and attach them to a VM.
# - Supports interface types: SATA, SCSI, VirtIO, and IDE.
# - Ensures that disks are not already assigned to active VMs.
# - Warns about disk sharing between multiple VMs to avoid data corruption.
# - Configures the selected disks for the VM and verifies the assignment.
# - Prefers persistent /dev/disk/by-id paths for assignment when available.
#
# The goal of this script is to simplify the process of assigning
# physical disks to Proxmox VMs, reducing manual configurations
# and preventing potential errors.
# ==========================================================


# Configuration ============================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"

if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
    LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
    UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
elif [[ ! -f "$UTILS_FILE" ]]; then
    UTILS_FILE="$BASE_DIR/utils.sh"
fi

# shellcheck source=/dev/null
if [[ -f "$UTILS_FILE" ]]; then
    source "$UTILS_FILE"
fi

# shellcheck source=/dev/null
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
    source "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh"
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
    source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
fi

BACKTITLE="ProxMenux"
UI_MENU_H=20
UI_MENU_W=84
UI_MENU_LIST_H=10
UI_SHORT_MENU_H=16
UI_SHORT_MENU_W=72
UI_SHORT_MENU_LIST_H=6
UI_MSG_H=10
UI_MSG_W=72
UI_YESNO_H=18
UI_YESNO_W=86
UI_RESULT_H=18
UI_RESULT_W=86

load_language
initialize_cache

if ! command -v pveversion >/dev/null 2>&1; then
    dialog --backtitle "$BACKTITLE" --title "$(translate "Error")" \
        --msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
    exit 1
fi
# ==========================================================


get_disk_info() {
    local disk=$1
    local model size
    model=$(lsblk -dn -o MODEL "$disk" | xargs)
    size=$(lsblk -dn -o SIZE "$disk" | xargs)
    [[ -z "$model" ]] && model="Unknown"
    printf '%s\t%s\n' "$model" "$size"
}

get_all_disk_paths() {
    local disk="$1"
    local real_path
    real_path=$(readlink -f "$disk" 2>/dev/null)

    [[ -n "$disk" ]] && echo "$disk"
    [[ -n "$real_path" ]] && echo "$real_path"

    local link
    for link in /dev/disk/by-id/* /dev/disk/by-path/*; do
        [[ -e "$link" ]] || continue
        [[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
        echo "$link"
    done | sort -u
}

get_preferred_disk_path() {
    local disk="$1"
    local real_path
    real_path=$(readlink -f "$disk" 2>/dev/null)
    [[ -z "$real_path" ]] && { echo "$disk"; return 0; }

    local best="" best_score=99999
    local link name score
    for link in /dev/disk/by-id/*; do
        [[ -e "$link" ]] || continue
        [[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
        name=$(basename "$link")
        [[ "$name" == *-part* ]] && continue

        case "$name" in
            ata-*|scsi-*|nvme-*) score=100 ;;
            wwn-*) score=200 ;;
            *) score=300 ;;
        esac
        score=$((score + ${#name}))
        if (( score < best_score )); then
            best="$link"
            best_score=$score
        fi
    done

    if [[ -n "$best" ]]; then
        echo "$best"
    else
        echo "$disk"
    fi
}

disk_referenced_in_config() {
    local config_text="$1"
    local disk="$2"
    local alias
    while read -r alias; do
        [[ -z "$alias" ]] && continue
        if grep -Fq "$alias" <<< "$config_text"; then
            return 0
        fi
    done < <(get_all_disk_paths "$disk")
    return 1
}


# ── DIALOG PHASE ──────────────────────────────────────────────────────────────

VM_LIST=$(qm list | awk 'NR>1 {print $1, $2}')
if [ -z "$VM_LIST" ]; then
    dialog --backtitle "$BACKTITLE" \
           --title "$(translate "Error")" \
           --msgbox "$(translate "No VMs available in the system.")" $UI_MSG_H $UI_MSG_W
    exit 1
fi

# shellcheck disable=SC2086
VMID=$(dialog --backtitle "$BACKTITLE" \
              --title "$(translate "Select VM")" \
              --menu "$(translate "Select the VM to which you want to add disks:")" $UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
              $VM_LIST \
              2>&1 >/dev/tty)

if [ -z "$VMID" ]; then
    dialog --backtitle "$BACKTITLE" \
           --title "$(translate "Error")" \
           --msgbox "$(translate "No VM was selected.")" $UI_MSG_H $UI_MSG_W
    exit 1
fi

VMID=$(echo "$VMID" | tr -d '"')

VM_STATUS=$(qm status "$VMID" | awk '{print $2}')
if [ "$VM_STATUS" == "running" ]; then
    dialog --backtitle "$BACKTITLE" \
           --title "$(translate "Warning")" \
           --msgbox "$(translate "The VM is powered on. Turn it off before adding disks.")" $UI_MSG_H $UI_MSG_W
    exit 1
fi


# ── TERMINAL PHASE 1: detect disks ────────────────────────────────────────────
show_proxmenux_logo
msg_title "$(translate "Import Disk to VM")"
msg_ok "$(translate "VM $VMID selected successfully.")"
msg_info "$(translate "Detecting available disks...")"

_refresh_host_storage_cache
VM_CONFIG=$(qm config "$VMID" 2>/dev/null | grep -vE '^\s*#|^description:')

FREE_DISKS=()

while read -r DISK; do
    [[ "$DISK" =~ /dev/zd ]] && continue

    IFS=$'\t' read -r MODEL SIZE < <(get_disk_info "$DISK")
    LABEL=""
    SHOW_DISK=true
    IS_MOUNTED=false
    IS_RAID=false
    IS_ZFS=false
    IS_LVM=false

    while read -r part fstype; do
        [[ "$fstype" == "zfs_member" ]] && IS_ZFS=true
        [[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true
        [[ "$fstype" == "LVM2_member" ]] && IS_LVM=true
        if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then
            IS_MOUNTED=true
        fi
    done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2)

    REAL_PATH=$(readlink -f "$DISK")
    if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
        IS_MOUNTED=true
    fi

    USED_BY=""
    if _disk_used_in_guest_configs "$DISK"; then
        USED_BY="⚠ $(translate "In use")"
    fi

    if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
        if grep -q "active raid" /proc/mdstat; then
            SHOW_DISK=false
        fi
    fi

    if $IS_ZFS; then
        SHOW_DISK=false
    fi

    # Catch whole-disk ZFS vdevs with no partitions (e.g. bare NVMe ZFS)
    # The tail -n +2 trick misses them; ZFS_DISKS from _refresh_host_storage_cache covers them.
    if [[ -n "$ZFS_DISKS" ]] && \
       { grep -qFx "$DISK" <<< "$ZFS_DISKS" || \
         { [[ -n "$REAL_PATH" ]] && grep -qFx "$REAL_PATH" <<< "$ZFS_DISKS"; }; }; then
        SHOW_DISK=false
    fi

    if $IS_MOUNTED; then
        SHOW_DISK=false
    fi

    if disk_referenced_in_config "$VM_CONFIG" "$DISK"; then
        SHOW_DISK=false
    fi

    if $SHOW_DISK; then
        [[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]"
        [[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID"
        [[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM"
        [[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS"

        DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
        FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
    fi
done < <(lsblk -dn -e 7,11 -o PATH)

if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
    stop_spinner
    dialog --backtitle "$BACKTITLE" \
           --title "$(translate "Error")" \
           --msgbox "$(translate "No disks available for this VM.")" $UI_MSG_H $UI_MSG_W
    exit 1
fi

stop_spinner
msg_ok "$(translate "Available disks detected.")"


# ── DIALOG PHASE: select disks + interface ────────────────────────────────────

MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
TOTAL_WIDTH=$((MAX_WIDTH + 20))
if [ $TOTAL_WIDTH -lt $UI_MENU_W ]; then
    TOTAL_WIDTH=$UI_MENU_W
fi
if [ $TOTAL_WIDTH -gt 116 ]; then
    TOTAL_WIDTH=116
fi

SELECTED=$(dialog --backtitle "$BACKTITLE" \
    --title "$(translate "Select Disks")" \
    --checklist "\n$(translate "Select the disks you want to add:")" $UI_MENU_H $TOTAL_WIDTH $UI_MENU_LIST_H \
    "${FREE_DISKS[@]}" \
    2>&1 >/dev/tty)

if [ -z "$SELECTED" ]; then
    dialog --backtitle "$BACKTITLE" \
           --title "$(translate "Error")" \
           --msgbox "$(translate "No disks were selected.")" $UI_MSG_H $UI_MSG_W
    exit 1
fi

INTERFACE=$(dialog --backtitle "$BACKTITLE" \
    --title "$(translate "Interface Type")" \
    --menu "$(translate "Select the interface type for all disks:")" $UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
    "sata"   "$(translate "Add as SATA")" \
    "scsi"   "$(translate "Add as SCSI")" \
    "virtio" "$(translate "Add as VirtIO")" \
    "ide"    "$(translate "Add as IDE")" \
    2>&1 >/dev/tty)

if [ -z "$INTERFACE" ]; then
    dialog --backtitle "$BACKTITLE" \
           --title "$(translate "Error")" \
           --msgbox "$(translate "No interface type was selected for the disks.")" $UI_MSG_H $UI_MSG_W
    exit 1
fi


# ── DIALOG PHASE: per-disk pre-check ──────────────────────────────────────────
declare -a DISK_LIST=()
declare -a DISK_DESCRIPTIONS=()
declare -a DISK_ASSIGNED_TOS=()
declare -a NVME_SKIPPED=()

for DISK in $SELECTED; do
    DISK="${DISK//\"/}"
    DISK_INFO=$(get_disk_info "$DISK")

    ASSIGNED_TO=""
    RUNNING_VMS=""
    RUNNING_CTS=""

    while read -r VM_ID VM_NAME; do
        VM_CONFIG_RAW=$(qm config "$VM_ID" 2>/dev/null)
        if [[ "$VM_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$VM_CONFIG_RAW" "$DISK"; then
            ASSIGNED_TO+="VM $VM_ID $VM_NAME\n"
            VM_STATUS_CHK=$(qm status "$VM_ID" | awk '{print $2}')
            [[ "$VM_STATUS_CHK" == "running" ]] && RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
        fi
    done < <(qm list | awk 'NR>1 {print $1, $2}')

    while read -r CT_ID CT_NAME; do
        CT_CONFIG_RAW=$(pct config "$CT_ID" 2>/dev/null)
        if [[ "$CT_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$CT_CONFIG_RAW" "$DISK"; then
            ASSIGNED_TO+="CT $CT_ID $CT_NAME\n"
            CT_STATUS_CHK=$(pct status "$CT_ID" | awk '{print $2}')
            [[ "$CT_STATUS_CHK" == "running" ]] && RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
        fi
    done < <(pct list | awk 'NR>1 {print $1, $3}')

    if [ -n "$RUNNING_VMS" ] || [ -n "$RUNNING_CTS" ]; then
        dialog --backtitle "$BACKTITLE" \
               --title "$(translate "Disk In Use")" \
               --msgbox "$(translate "The disk") $DISK_INFO $(translate "is in use by the following running VM(s) or CT(s):")\\n$RUNNING_VMS$RUNNING_CTS\\n\\n$(translate "Stop them first and run this script again.")" $UI_RESULT_H $UI_RESULT_W
        continue
    fi

    if [ -n "$ASSIGNED_TO" ]; then
        if ! dialog --backtitle "$BACKTITLE" \
               --title "$(translate "Disk Already Assigned")" \
               --yesno "\n\n$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" $UI_YESNO_H $UI_YESNO_W; then
            continue
        fi
    fi

    # NVMe: suggest PCIe passthrough for better performance
    if [[ "$DISK" =~ /dev/nvme ]] || \
       [[ "$(lsblk -dn -o TRAN "$DISK" 2>/dev/null | xargs)" == "nvme" ]]; then
        NVME_CHOICE=$(dialog --backtitle "$BACKTITLE" \
            --title "$(translate "NVMe Disk Detected")" \
            --default-item "disk" \
            --menu "\n$(translate "Adding this NVMe as a PCIe device (via 'Add Controller or NVMe PCIe to VM') gives better performance.")\n\n$(translate "How do you want to add it?")" \
            $UI_YESNO_H $UI_YESNO_W 2 \
            "disk" "$(translate "Add as disk (standard)")" \
            "pci"  "$(translate "Skip — I will add it as PCIe device")" \
            2>&1 >/dev/tty)
        if [[ "$NVME_CHOICE" == "pci" ]]; then
            NVME_SKIPPED+=("$DISK")
            continue
        fi
    fi

    DISK_LIST+=("$DISK")
    DISK_DESCRIPTIONS+=("$DISK_INFO")
    DISK_ASSIGNED_TOS+=("$ASSIGNED_TO")
done

if [ "${#DISK_LIST[@]}" -eq 0 ]; then
    show_proxmenux_logo
    msg_title "$(translate "Import Disk to VM")"
    msg_warn "$(translate "No disks were configured for processing.")"
    echo ""
    msg_success "$(translate "Press Enter to return to menu...")"
    read -r
    exit 0
fi


# ── TERMINAL PHASE: execute all disk operations ───────────────────────────────
show_proxmenux_logo
msg_title "$(translate "Import Disk to VM")"
msg_ok "$(translate "VM $VMID selected successfully.")"
msg_ok "$(translate "Disks to process:") ${#DISK_LIST[@]}"
for i in "${!DISK_LIST[@]}"; do
    IFS=$'\t' read -r _desc_model _desc_size <<< "${DISK_DESCRIPTIONS[$i]}"
    echo -e "${TAB}${BL}${DISK_LIST[$i]}  $_desc_model  $_desc_size${CL}"
done
if [[ ${#NVME_SKIPPED[@]} -gt 0 ]]; then
    echo ""
    msg_warn "$(translate "NVMe skipped (to add as PCIe use 'Add Controller or NVMe PCIe to VM'):")"
    for _nvme in "${NVME_SKIPPED[@]}"; do
        echo -e "${TAB}${BL}${_nvme}${CL}"
    done
fi
echo ""
msg_ok "$(translate "Interface type:") $INTERFACE"
echo ""

DISKS_ADDED=0

for i in "${!DISK_LIST[@]}"; do
    DISK="${DISK_LIST[$i]}"
    ASSIGNED_TO="${DISK_ASSIGNED_TOS[$i]}"
    IFS=$'\t' read -r _model _size <<< "${DISK_DESCRIPTIONS[$i]}"

    INDEX=0
    while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do
        ((INDEX++))
    done

    ASSIGN_PATH=$(get_preferred_disk_path "$DISK")
    msg_info "$(translate "Adding") $_model $_size $(translate "as") ${INTERFACE}${INDEX}..."
    if RESULT=$(qm set "$VMID" "-${INTERFACE}${INDEX}" "$ASSIGN_PATH" 2>&1); then
        msg_ok "$(translate "Disk added as") ${INTERFACE}${INDEX} $(translate "using") $ASSIGN_PATH"
        [[ -n "$ASSIGNED_TO" ]] && msg_warn "$(translate "WARNING: This disk is also assigned to:") $(echo -e "$ASSIGNED_TO" | tr '\n' ' ')"
        ((DISKS_ADDED++))
    else
        msg_error "$(translate "Could not add") $_model $_size: $RESULT"
    fi
done

echo ""
if [ "$DISKS_ADDED" -gt 0 ]; then
    msg_ok "$(translate "Completed.") $DISKS_ADDED $(translate "disk(s) added to VM") $VMID."
else
    msg_warn "$(translate "No disks were added.")"
fi
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 0
