#!/bin/bash
# ==========================================================
# ProxMenux - Export VM to OVA or OVF
# ==========================================================
# Author      : MacRimi
# Copyright   : (c) 2024 MacRimi
# License     : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version     : 1.0
# Last Updated: 07/04/2026
# ==========================================================

LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"

if [[ -f "$UTILS_FILE" ]]; then
    source "$UTILS_FILE"
fi

load_language
initialize_cache

require_cmd() {
    local cmd="$1"
    if ! command -v "$cmd" >/dev/null 2>&1; then
        dialog --backtitle "ProxMenux" --title "$(translate "Missing dependency")" \
            --msgbox "$(translate "Required command not found:") $cmd" 8 60
        return 1
    fi
    return 0
}

human_bytes() {
    local bytes="$1"
    local units=("B" "KB" "MB" "GB" "TB" "PB")
    local idx=0
    local value="$bytes"

    [[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]] && { echo "N/A"; return; }

    while [[ "$value" -ge 1024 && "$idx" -lt 5 ]]; do
        value=$((value / 1024))
        idx=$((idx + 1))
    done

    echo "${value}${units[$idx]}"
}

sanitize_name() {
    local raw="$1"
    local out
    out=$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-zA-Z0-9._-]/_/g' | sed 's/__*/_/g' | sed 's/^_\+//;s/_\+$//')
    [[ -z "$out" ]] && out="vm"
    echo "$out"
}

xml_escape() {
    local s="$1"
    s=${s//&/&amp;}
    s=${s//</&lt;}
    s=${s//>/&gt;}
    s=${s//\"/&quot;}
    s=${s//\'/&apos;}
    echo "$s"
}

validate_destination_dir() {
    local dir="$1"
    if [[ ! -d "$dir" ]]; then
        dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
            --msgbox "$(translate "Destination directory does not exist:")\n$dir" 8 74
        return 1
    fi

    if [[ ! -w "$dir" ]]; then
        dialog --backtitle "ProxMenux" --title "$(translate "Permission error")" \
            --msgbox "$(translate "Destination directory is not writable:")\n$dir" 8 70
        return 1
    fi

    return 0
}

select_vm() {
    local options=()
    local line vmid name status

    while read -r line; do
        [[ -z "$line" ]] && continue
        vmid=$(echo "$line" | awk '{print $1}')
        name=$(echo "$line" | awk '{print $2}')
        status=$(echo "$line" | awk '{print $3}')
        [[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
        [[ -z "$name" ]] && name="vm-${vmid}"
        options+=("$vmid" "$name [$status]")
    done < <(qm list 2>/dev/null)

    if [[ ${#options[@]} -eq 0 ]]; then
        dialog --backtitle "ProxMenux" --title "$(translate "No VMs found")" \
            --msgbox "$(translate "No virtual machines were found on this host.")" 8 60
        return 1
    fi

    VMID=$(dialog --backtitle "ProxMenux" --title "$(translate "Export VM to OVA or OVF")" \
        --menu "$(translate "Select VM to export:")" 20 80 12 \
        "${options[@]}" 3>&1 1>&2 2>&3)

    [[ -n "$VMID" ]] || return 1
    return 0
}

ensure_vm_stopped() {
    local status
    status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')

    if [[ "$status" == "stopped" ]]; then
        return 0
    fi

    if ! dialog --backtitle "ProxMenux" --title "$(translate "VM is running")" --yesno \
        "$(translate "For a consistent export, the VM should be stopped.")\n\n$(translate "Do you want ProxMenux to stop it now?")" 10 70; then
        return 1
    fi

    qm shutdown "$VMID" --timeout 120 >/dev/null 2>&1 || true

    local i
    for i in $(seq 1 60); do
        status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
        [[ "$status" == "stopped" ]] && return 0
        sleep 2
    done

    if dialog --backtitle "ProxMenux" --title "$(translate "Shutdown timeout")" --yesno \
        "$(translate "Graceful shutdown timed out.")\n\n$(translate "Force stop VM now?")" 10 60; then
        qm stop "$VMID" >/dev/null 2>&1 || true
        sleep 2
        status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
        [[ "$status" == "stopped" ]] && return 0
    fi

    dialog --backtitle "ProxMenux" --title "$(translate "Cannot continue")" \
        --msgbox "$(translate "VM is still running. Export cancelled.")" 8 60
    return 1
}

select_export_mode() {
    EXPORT_MODE=$(dialog --backtitle "ProxMenux" --title "$(translate "Export Format")" \
        --menu "$(translate "Select export format:")" 14 70 4 \
        "ova" "$(translate "OVA (single portable file)")" \
        "ovf" "$(translate "OVF (descriptor + VMDK files)")" \
        3>&1 1>&2 2>&3)
    [[ -n "$EXPORT_MODE" ]] || return 1
    return 0
}

select_destination_dir() {
    local dump_dir="/var/lib/vz/dump"
    local iso_dir="/var/lib/vz/template/iso"
    local options=(
        "1" "$dump_dir          [$(translate "recommended")]"
        "2" "$iso_dir  [$(translate "recommended")]"
        "M" "$(translate "Manual path entry")"
    )

    while true; do
        local choice
        choice=$(dialog --backtitle "ProxMenux" --title "$(translate "Destination Directory")" \
            --menu "$(translate "Select where to export VM files (OVA/OVF + temporary workspace):")" \
            16 84 8 "${options[@]}" 3>&1 1>&2 2>&3)

        [[ -n "$choice" ]] || return 1

        case "$choice" in
            M)
                DEST_DIR=$(dialog --backtitle "ProxMenux" --title "$(translate "Manual destination path")" \
                    --inputbox "$(translate "Enter destination directory for exported file(s):")" \
                    10 90 "/var/lib/vz/dump" 3>&1 1>&2 2>&3)
                [[ -n "$DEST_DIR" ]] || continue
                if [[ ! -d "$DEST_DIR" ]]; then
                    if dialog --backtitle "ProxMenux" --title "$(translate "Create directory")" \
                        --yesno "$(translate "The selected directory does not exist:")\n$DEST_DIR\n\n$(translate "Do you want to create it now?")" \
                        11 80; then
                        if ! mkdir -p "$DEST_DIR" 2>/dev/null; then
                            dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
                                --msgbox "$(translate "Could not create destination directory:")\n$DEST_DIR" 8 74
                            continue
                        fi
                    else
                        continue
                    fi
                fi
                validate_destination_dir "$DEST_DIR" || continue
                return 0
                ;;
            1)
                DEST_DIR="$dump_dir"
                validate_destination_dir "$DEST_DIR" || continue
                return 0
                ;;
            2)
                DEST_DIR="$iso_dir"
                validate_destination_dir "$DEST_DIR" || continue
                return 0
                ;;
            *)
                continue
                ;;
        esac
    done
}

get_vm_metadata() {
    VM_CONF=$(qm config "$VMID" 2>/dev/null) || return 1

    VM_NAME=$(echo "$VM_CONF" | awk -F': ' '/^name:/{print $2; exit}')
    [[ -z "$VM_NAME" ]] && VM_NAME="vm-${VMID}"

    VM_MEMORY=$(echo "$VM_CONF" | awk -F': ' '/^memory:/{print $2; exit}')
    [[ -z "$VM_MEMORY" ]] && VM_MEMORY=1024

    VM_CORES=$(echo "$VM_CONF" | awk -F': ' '/^cores:/{print $2; exit}')
    VM_SOCKETS=$(echo "$VM_CONF" | awk -F': ' '/^sockets:/{print $2; exit}')
    [[ -z "$VM_CORES" ]] && VM_CORES=1
    [[ -z "$VM_SOCKETS" ]] && VM_SOCKETS=1
    VM_VCPUS=$((VM_CORES * VM_SOCKETS))

    VM_OSTYPE=$(echo "$VM_CONF" | awk -F': ' '/^ostype:/{print $2; exit}')
    case "$VM_OSTYPE" in
        l26|l24) VM_OS_DESC="Linux" ;;
        win11|win10|win8|win7|w2k8|w2k12|w2k16|w2k19|w2k22|wxp|w2k|w2k3)
            VM_OS_DESC="Windows"
            ;;
        *) VM_OS_DESC="Other" ;;
    esac

    NET_COUNT=$(echo "$VM_CONF" | grep -E '^net[0-9]+:' | wc -l)
}

get_virtual_size_bytes() {
    local src="$1"
    local bytes=""

    bytes=$(qemu-img info "$src" 2>/dev/null | sed -n 's/.*virtual size:.*(\([0-9]\+\) bytes).*/\1/p' | head -1)
    if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
        echo "$bytes"
        return 0
    fi

    if [[ -b "$src" ]]; then
        bytes=$(blockdev --getsize64 "$src" 2>/dev/null || true)
        if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
            echo "$bytes"
            return 0
        fi
    fi

    bytes=$(stat -c%s "$src" 2>/dev/null || true)
    if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
        echo "$bytes"
        return 0
    fi

    echo "0"
    return 0
}

collect_vm_disks() {
    DISK_COUNT=0
    unset DISK_SLOTS DISK_SRCS DISK_VSIZES
    declare -ga DISK_SLOTS DISK_SRCS DISK_VSIZES

    local line slot value source src

    while IFS= read -r line; do
        if [[ "$line" =~ ^(scsi|sata|virtio|ide)[0-9]+: ]]; then
            slot="${line%%:*}"
            value="${line#*: }"

            [[ "$value" == *"media=cdrom"* ]] && continue
            [[ "$value" == *"cloudinit"* ]] && continue

            source="${value%%,*}"
            [[ -z "$source" || "$source" == "none" ]] && continue

            src=""
            if [[ "$source" == /dev/* || "$source" == /* ]]; then
                src="$source"
            elif [[ "$source" == *:* ]]; then
                src=$(pvesm path "$source" 2>/dev/null || true)
            fi

            if [[ -z "$src" || ! -e "$src" ]]; then
                continue
            fi

            DISK_SLOTS+=("$slot")
            DISK_SRCS+=("$src")
            DISK_VSIZES+=("$(get_virtual_size_bytes "$src")")
            DISK_COUNT=$((DISK_COUNT + 1))
        fi
    done <<< "$VM_CONF"

    [[ "$DISK_COUNT" -gt 0 ]] || return 1
    return 0
}

check_destination_space() {
    local total=0
    local i
    for i in "${DISK_VSIZES[@]}"; do
        [[ "$i" =~ ^[0-9]+$ ]] && total=$((total + i))
    done

    local factor=120
    [[ "$EXPORT_MODE" == "ova" ]] && factor=220
    REQUIRED_BYTES=$((total * factor / 100))

    AVAILABLE_BYTES=$(df -PB1 "$DEST_DIR" | awk 'NR==2{print $4}')
    [[ "$AVAILABLE_BYTES" =~ ^[0-9]+$ ]] || AVAILABLE_BYTES=0

    if [[ "$AVAILABLE_BYTES" -lt "$REQUIRED_BYTES" ]]; then
        if ! dialog --backtitle "ProxMenux" --title "$(translate "Low free space warning")" --yesno \
            "$(translate "Estimated required free space:") $(human_bytes "$REQUIRED_BYTES") ($REQUIRED_BYTES bytes)\n$(translate "Current free space:") $(human_bytes "$AVAILABLE_BYTES") ($AVAILABLE_BYTES bytes)\n\n$(translate "Do you want to continue anyway?")" 13 90; then
            return 1
        fi
    fi

    return 0
}

generate_ovf_descriptor() {
    local ovf_path="$1"
    local vm_name_xml os_desc_xml
    vm_name_xml=$(xml_escape "$VM_NAME")
    os_desc_xml=$(xml_escape "$VM_OS_DESC")

    {
        echo '<?xml version="1.0" encoding="UTF-8"?>'
        echo '<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">'
        echo '  <References>'
    } > "$ovf_path"

    local idx file_id disk_id file_name file_size capacity
    for idx in "${!EXPORT_DISK_FILES[@]}"; do
        file_id="file$((idx + 1))"
        file_name="${EXPORT_DISK_FILES[$idx]}"
        file_size=$(stat -c%s "$WORK_DIR/$file_name")
        echo "    <File ovf:id=\"$file_id\" ovf:href=\"$file_name\" ovf:size=\"$file_size\"/>" >> "$ovf_path"
    done

    {
        echo '  </References>'
        echo '  <DiskSection>'
        echo '    <Info>Virtual disk information</Info>'
    } >> "$ovf_path"

    for idx in "${!EXPORT_DISK_FILES[@]}"; do
        file_id="file$((idx + 1))"
        disk_id="vmdisk$((idx + 1))"
        capacity="${DISK_VSIZES[$idx]}"
        [[ -z "$capacity" || "$capacity" -le 0 ]] && capacity=$(stat -c%s "$WORK_DIR/${EXPORT_DISK_FILES[$idx]}")
        echo "    <Disk ovf:diskId=\"$disk_id\" ovf:fileRef=\"$file_id\" ovf:capacity=\"$capacity\" ovf:capacityAllocationUnits=\"byte\" ovf:format=\"http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized\"/>" >> "$ovf_path"
    done

    {
        echo '  </DiskSection>'
        echo "  <VirtualSystem ovf:id=\"$(sanitize_name "$VM_NAME")\">"
        echo '    <Info>A virtual machine</Info>'
        echo "    <Name>$vm_name_xml</Name>"
        echo '    <OperatingSystemSection ovf:id="94">'
        echo '      <Info>Guest operating system</Info>'
        echo "      <Description>$os_desc_xml</Description>"
        echo '    </OperatingSystemSection>'
        echo '    <VirtualHardwareSection>'
        echo '      <Info>Virtual hardware requirements</Info>'
        echo '      <System>'
        echo '        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>'
        echo '        <vssd:InstanceID>0</vssd:InstanceID>'
        echo '        <vssd:VirtualSystemIdentifier>vm</vssd:VirtualSystemIdentifier>'
        echo '        <vssd:VirtualSystemType>vmx-14</vssd:VirtualSystemType>'
        echo '      </System>'
        echo '      <Item>'
        echo '        <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>'
        echo '        <rasd:Description>Number of Virtual CPUs</rasd:Description>'
        echo '        <rasd:ElementName>Virtual CPU(s)</rasd:ElementName>'
        echo '        <rasd:InstanceID>1</rasd:InstanceID>'
        echo "        <rasd:VirtualQuantity>$VM_VCPUS</rasd:VirtualQuantity>"
        echo '        <rasd:ResourceType>3</rasd:ResourceType>'
        echo '      </Item>'
        echo '      <Item>'
        echo '        <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>'
        echo '        <rasd:Description>Memory Size</rasd:Description>'
        echo '        <rasd:ElementName>Memory</rasd:ElementName>'
        echo '        <rasd:InstanceID>2</rasd:InstanceID>'
        echo "        <rasd:VirtualQuantity>$VM_MEMORY</rasd:VirtualQuantity>"
        echo '        <rasd:ResourceType>4</rasd:ResourceType>'
        echo '      </Item>'
        echo '      <Item>'
        echo '        <rasd:Address>0</rasd:Address>'
        echo '        <rasd:Description>SCSI Controller</rasd:Description>'
        echo '        <rasd:ElementName>SCSI Controller 0</rasd:ElementName>'
        echo '        <rasd:InstanceID>10</rasd:InstanceID>'
        echo '        <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>'
        echo '        <rasd:ResourceType>6</rasd:ResourceType>'
        echo '      </Item>'
    } >> "$ovf_path"

    for idx in "${!EXPORT_DISK_FILES[@]}"; do
        disk_id="vmdisk$((idx + 1))"
        echo '      <Item>' >> "$ovf_path"
        echo "        <rasd:AddressOnParent>$idx</rasd:AddressOnParent>" >> "$ovf_path"
        echo '        <rasd:Description>Hard disk</rasd:Description>' >> "$ovf_path"
        echo "        <rasd:ElementName>Hard disk $((idx + 1))</rasd:ElementName>" >> "$ovf_path"
        echo "        <rasd:HostResource>ovf:/disk/$disk_id</rasd:HostResource>" >> "$ovf_path"
        echo "        <rasd:InstanceID>$((200 + idx + 1))</rasd:InstanceID>" >> "$ovf_path"
        echo '        <rasd:Parent>10</rasd:Parent>' >> "$ovf_path"
        echo '        <rasd:ResourceType>17</rasd:ResourceType>' >> "$ovf_path"
        echo '      </Item>' >> "$ovf_path"
    done

    if [[ "$NET_COUNT" -gt 0 ]]; then
        local n
        for n in $(seq 1 "$NET_COUNT"); do
            {
                echo '      <Item>'
                echo '        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>'
                echo '        <rasd:Connection>VM Network</rasd:Connection>'
                echo "        <rasd:ElementName>Ethernet adapter $n</rasd:ElementName>"
                echo "        <rasd:InstanceID>$((300 + n))</rasd:InstanceID>"
                echo '        <rasd:ResourceType>10</rasd:ResourceType>'
                echo '      </Item>'
            } >> "$ovf_path"
        done
    fi

    {
        echo '    </VirtualHardwareSection>'
        echo '  </VirtualSystem>'
        echo '</Envelope>'
    } >> "$ovf_path"
}

generate_manifest() {
    local mf_path="$1"
    shift
    local files=("$@")
    : > "$mf_path"

    local f hash
    for f in "${files[@]}"; do
        hash=$(sha1sum "$WORK_DIR/$f" | awk '{print $1}')
        echo "SHA1($f)= $hash" >> "$mf_path"
    done
}

print_export_result() {
    local mode="$1"
    local path="$2"

    echo ""
    msg_title "$(translate "Export Summary")"

    msg_ok "$(translate "VM:") ${VMID} — ${VM_NAME}"
    msg_ok "$(translate "vCPUs:") ${VM_VCPUS}    $(translate "Memory:") ${VM_MEMORY} MB    $(translate "Disks exported:") ${DISK_COUNT}"
    echo ""

    if [[ "$mode" == "ova" ]]; then
        local ova_size ova_sha1
        ova_size=$(stat -c%s "$path" 2>/dev/null || echo 0)
        ova_sha1=$(sha1sum "$path" 2>/dev/null | awk '{print $1}')
        msg_ok "$(translate "Format:") OVA — $(translate "single portable archive")"
        msg_ok "$(translate "File:") $path"
        msg_ok "$(translate "Size:") $(human_bytes "$ova_size")  (${ova_size} $(translate "bytes"))"
        msg_ok "SHA1: ${ova_sha1}"
    else
        local fsz total_size=0 f
        msg_ok "$(translate "Format:") OVF — $(translate "descriptor + VMDK files")"
        msg_ok "$(translate "Directory:") $path"
        for f in "${EXPORT_DISK_FILES[@]}"; do
            fsz=$(stat -c%s "$path/$f" 2>/dev/null || echo 0)
            total_size=$((total_size + fsz))
            msg_info2 "  ${f}  [$(human_bytes "$fsz")]"
        done
        msg_ok "$(translate "Total size:") $(human_bytes "$total_size")"
    fi

    echo ""
    msg_ok "$(translate "Compatible with:") VMware ESXi 6.7+ (vmx-14)  ·  VMware Workstation / Fusion  ·  VirtualBox  ·  Proxmox VE"
    msg_info2 "$(translate "Not portable:") $(translate "PCI passthrough, TPM state, cloud-init configuration, Proxmox hooks")"
    echo ""
}

run_export() {
    show_proxmenux_logo
    msg_title "$(translate "Export VM to OVA or OVF")"

    msg_ok "$(translate "VM selected:") $VMID ($VM_NAME)"
    msg_ok "$(translate "Export mode:") ${EXPORT_MODE^^}"
    msg_ok "$(translate "Destination:") $DEST_DIR"

    local ts vm_safe base_name
    ts=$(date +%Y%m%d_%H%M%S)
    vm_safe=$(sanitize_name "$VM_NAME")
    base_name="${vm_safe}-${VMID}-${ts}"

    WORK_DIR=$(mktemp -d "$DEST_DIR/.ovaovf-${base_name}-XXXXXX")
    if [[ ! -d "$WORK_DIR" ]]; then
        msg_error "$(translate "Could not create temporary working directory.")"
        return 1
    fi

    msg_ok "$(translate "Working directory:") $WORK_DIR"

    # Clean up temp dir on unexpected exit (Ctrl+C, unhandled error, etc.)
    trap 'rm -rf "$WORK_DIR" 2>/dev/null' EXIT

    declare -ga EXPORT_DISK_FILES
    EXPORT_DISK_FILES=()

    local i src dst disk_name
    for i in "${!DISK_SRCS[@]}"; do
        src="${DISK_SRCS[$i]}"
        disk_name="${base_name}-disk$((i + 1)).vmdk"
        dst="$WORK_DIR/$disk_name"

        echo ""
        msg_info "$(translate "Converting disk") $((i + 1))/$DISK_COUNT: ${DISK_SLOTS[$i]}"
        msg_info2 "$(translate "Source:") $src"

        if ! qemu-img convert -p -O vmdk -o subformat=streamOptimized "$src" "$dst"; then
            msg_error "$(translate "Disk conversion failed for") ${DISK_SLOTS[$i]}"
            return 1
        fi

        EXPORT_DISK_FILES+=("$disk_name")
        msg_ok "$(translate "Converted:") $disk_name"
    done

    local ovf_file mf_file
    ovf_file="${base_name}.ovf"
    mf_file="${base_name}.mf"

    msg_info "$(translate "Generating OVF descriptor...")"
    generate_ovf_descriptor "$WORK_DIR/$ovf_file"

    msg_info "$(translate "Generating manifest...")"
    generate_manifest "$WORK_DIR/$mf_file" "$ovf_file" "${EXPORT_DISK_FILES[@]}"

    if [[ "$EXPORT_MODE" == "ovf" ]]; then
        local final_dir="$DEST_DIR/${base_name}-ovf"
        rm -rf "$final_dir"
        trap - EXIT
        mv "$WORK_DIR" "$final_dir"

        print_export_result "ovf" "$final_dir"
        return 0
    fi

    local ova_path="$DEST_DIR/${base_name}.ova"
    msg_info "$(translate "Packaging OVA file...")"

    if ! tar -C "$WORK_DIR" -cf "$ova_path" "$ovf_file" "$mf_file" "${EXPORT_DISK_FILES[@]}"; then
        msg_error "$(translate "Failed to create OVA archive.")"
        return 1
    fi

    trap - EXIT
    rm -rf "$WORK_DIR"

    print_export_result "ova" "$ova_path"
    return 0
}

main() {
    require_cmd dialog || exit 1
    require_cmd qm || exit 1
    require_cmd pvesm || exit 1
    require_cmd qemu-img || exit 1
    require_cmd tar || exit 1
    require_cmd sha1sum || exit 1

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

    select_vm || exit 0
    ensure_vm_stopped || exit 0
    select_export_mode || exit 0
    select_destination_dir || exit 0

    get_vm_metadata || {
        dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
            --msgbox "$(translate "Could not read VM configuration.")" 8 60
        exit 1
    }

    collect_vm_disks || {
        dialog --backtitle "ProxMenux" --title "$(translate "No exportable disks")" \
            --msgbox "$(translate "No exportable VM disks were found (CD-ROM/cloud-init are excluded).")" 9 80
        exit 1
    }

    check_destination_space || exit 0

    if ! dialog --backtitle "ProxMenux" --title "$(translate "Confirm export")" --yesno \
        "$(translate "VM:") $VMID ($VM_NAME)\n$(translate "Disks to export:") $DISK_COUNT\n$(translate "Format:") ${EXPORT_MODE^^}\n$(translate "Destination:") $DEST_DIR\n\n$(translate "Continue?")" 13 80; then
        exit 0
    fi

    if run_export; then
        echo ""
        msg_success "$(translate "Press Enter to return...")"
        read -r
        exit 0
    else
        echo ""
        msg_error "$(translate "Export failed.")"
        msg_info2 "$(translate "Temporary working directory (if present):") $WORK_DIR"
        msg_success "$(translate "Press Enter to return...")"
        read -r
        exit 1
    fi
}

main "$@"
