#!/usr/bin/env bash
#
# Copyright © 2020 VMware, Inc.
# Copyright © 2026 NiceSOFT, LLC
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-only
#
# NiceOS image build helper.
#
# Purpose
# -------
# This script is a high-level wrapper around the existing image builder
# container commands and host-side image conversion tools.
#
# It intentionally keeps the original container subcommands unchanged:
#
#   create-repo
#   create-image
#   create-azure
#   create-ova
#
# Those names are normally defined by the Docker image entrypoint and by the
# existing Photon/NiceOS image builder toolchain. Renaming them here would break
# compatibility. The script itself, its defaults, variables and documentation
# are NiceOS-oriented.
#
# What this script can produce
# ----------------------------
# 1. Base RAW image
#      Produced by the container command:
#        create-image -c <ks> -v <release>
#
# 2. Host-side converted images, produced from RAW through qemu-img:
#      raw
#      raw.gz
#      raw.xz
#      qcow2
#      qcow2-compressed
#      vmdk
#      vdi
#      vhd-fixed     qemu-img format: vpc, subformat=fixed
#      vhd-dynamic   qemu-img format: vpc, subformat=dynamic
#      vhdx
#
# 3. VMware-style artifacts through the existing container helper:
#      ova
#      ovf
#      ovf + mf
#      vmdk-only
#
# 4. Azure artifact through the existing container helper:
#      azure
#
# 5. Proxmox helper bundle:
#      Proxmox does not need a unique disk format. It imports raw/qcow2/vmdk
#      using qm importdisk. For this reason the script creates:
#
#        proxmox/<name>-proxmox-import.sh
#        proxmox/<name>-proxmox-notes.txt
#
#      and, when requested, ensures a qcow2 image exists.
#
# Recommended workflow
# --------------------
# Build the raw image once, then ask for all needed derivative formats:
#
#   niceos-create-image.sh \
#     --config-file minimal_ks.yaml \
#     --raw-image minimal.img \
#     --local-repo-path /DATA/niceos/repo/5.0 \
#     --niceos-installer-path /DATA/src/niceos-os-installer \
#     --flavor ova \
#     --ova-config minimal.yaml \
#     --ova-name niceos-minimal \
#     --formats all \
#     --output-dir /DATA/niceos/images/minimal \
#     --releasever 5.0
#
# Notes
# -----
# * The RAW file name must match what your kickstart/config actually creates.
# * If RAW_IMAGE is relative, it is considered relative to examples/<flavor>.
# * If RAW_IMAGE is absolute, it is used as-is.
# * Additional converted images are written into --output-dir.
# * Docker may still need sudo unless the current user can access docker.sock.
# * qemu-img is required only for host-side conversion formats.
#

set -Eeuo pipefail
set +h

SCRIPT_NAME="$(basename "$0")"

DEFAULT_RELEASE_VERSION="5.0"
RELEASE_VERSION="${RELEASE_VERSION:-$DEFAULT_RELEASE_VERSION}"

CREATE_OVF="false"
CREATE_MF="false"
VMDK_ONLY="false"
DRY_RUN="false"

SKIP_REPO_CREATE="false"
SKIP_RAW_CREATE="false"
KEEP_GOING="false"

SRC_REPO_URL=""
RAW_IMAGE=""
KS_CONFIG_FILE=""
LOCAL_REPO_PATH=""
NICEOS_INSTALLER_PATH="${NICEOS_INSTALLER_PATH:-${POI_PATH:-}}"
IMAGE_FLAVOR=""
OVA_CONFIG=""
OVA_NAME=""
ARCH="${ARCH:-}"
OUTPUT_DIR=""

FORMATS_RAW=""
FORMATS=()

SYSTEM_ARCH_RAW="$(uname -m)"
SYSTEM_DOCKER_ARCH=""
DOCKER_ARCH=""

DOCKER_BIN="${DOCKER_BIN:-docker}"
DOCKER_WITH_SUDO="${DOCKER_WITH_SUDO:-auto}"
NICEOS_IMAGE_NAME="${NICEOS_IMAGE_NAME:-${POI_IMAGE_NAME:-niceos/installer}}"
CONTAINER_IMAGE=""

QEMU_IMG_BIN="${QEMU_IMG_BIN:-qemu-img}"
GZIP_BIN="${GZIP_BIN:-gzip}"
XZ_BIN="${XZ_BIN:-xz}"

PROXMOX_VM_NAME=""
PROXMOX_STORAGE="${PROXMOX_STORAGE:-local-lvm}"
PROXMOX_BRIDGE="${PROXMOX_BRIDGE:-vmbr0}"
PROXMOX_VMID="${PROXMOX_VMID:-9000}"
PROXMOX_MEMORY="${PROXMOX_MEMORY:-2048}"
PROXMOX_CORES="${PROXMOX_CORES:-2}"
PROXMOX_DISK_BUS="${PROXMOX_DISK_BUS:-scsi}"
PROXMOX_NET_MODEL="${PROXMOX_NET_MODEL:-virtio}"
PROXMOX_OS_TYPE="${PROXMOX_OS_TYPE:-l26}"

FLAVOR_DIR=""
RAW_IMAGE_HOST_PATH=""
OUTPUT_DIR_ABS=""

EXTRA_DOCKER_MOUNTS=()
MAP_RESULT=""

SUPPORTED_FLAVORS=("azure" "ova" "ami" "rpi")
SUPPORTED_ARCHES=("x86_64" "aarch64" "amd64" "arm64")

# Host-side formats supported by this wrapper.
# "all" expands to most useful portable formats plus proxmox helper.
HOST_FORMATS=(
    "raw"
    "raw.gz"
    "raw.xz"
    "qcow2"
    "qcow2-compressed"
    "vmdk"
    "vdi"
    "vhd-fixed"
    "vhd-dynamic"
    "vhdx"
    "proxmox"
)

# Container-side finalizers. They require existing builder commands inside
# the image and preserve old upstream behavior.
CONTAINER_FORMATS=(
    "azure"
    "ova"
    "ovf"
    "ovf-mf"
    "vmdk-only"
)

ALL_FORMATS=(
    "raw"
    "raw.gz"
    "raw.xz"
    "qcow2"
    "qcow2-compressed"
    "vmdk"
    "vdi"
    "vhd-fixed"
    "vhd-dynamic"
    "vhdx"
    "proxmox"
    "azure"
    "ova"
    "ovf"
    "ovf-mf"
    "vmdk-only"
)


log() {
    printf '[%s] %s\n' "$SCRIPT_NAME" "$*" >&2
}


die() {
    log "ERROR: $*"
    exit 1
}


warn() {
    log "WARNING: $*"
}


usage() {
    cat >&2 <<'EOF'
Usage:
  niceos-create-image.sh
      --config-file <ks-config-file>
      --local-repo-path <local-repo-path>
      --niceos-installer-path <path-to-niceos-os-installer>
      --flavor <azure|ova|ami|rpi>
      [--raw-image <image-file>]
      [--src-repo-url <src-repo-url>]
      [--ova-config <ova-config>]
      [--ova-name <ova-name>]
      [--formats <comma-separated-list|all>]
      [--output-dir <dir>]
      [--ovf]
      [--mf]
      [--vmdk-only]
      [--arch <x86_64|aarch64|amd64|arm64>]
      [--releasever <release-version>]
      [--skip-repo-create]
      [--skip-raw-create]
      [--keep-going]
      [--dry-run]

Compatibility aliases:
  --poi-path is accepted as an alias for --niceos-installer-path.
  POI_PATH and POI_IMAGE_NAME environment variables are accepted.

Required:
  --config-file             Kickstart/config YAML file.
  --local-repo-path         Local RPM repository cache path mounted into the container as /repo.
  --niceos-installer-path   Path to the niceos-os-installer source tree.
  --flavor                  Builder flavor directory: azure, ova, ami, rpi.

Usually required:
  --raw-image               RAW image file produced by create-image.
                            Required for conversion formats, azure, ova, ovf,
                            ovf-mf and vmdk-only.

Required for OVA/OVF/VMDK container finalizers:
  --ova-config              OVA configuration file.
  --ova-name                OVA/VM name.

Output format selection:
  --formats raw
  --formats qcow2
  --formats qcow2,vmdk,proxmox
  --formats all

Supported --formats values:
  raw
      Copy/preserve the generated RAW image in --output-dir.

  raw.gz
      Create gzip-compressed RAW image.

  raw.xz
      Create xz-compressed RAW image.

  qcow2
      Create qcow2 image with qemu-img.

  qcow2-compressed
      Create compressed qcow2 image with qemu-img -c.

  vmdk
      Create VMDK image with qemu-img.

  vdi
      Create VirtualBox VDI image with qemu-img.

  vhd-fixed
      Create fixed VHD image using qemu-img vpc subformat=fixed.
      Useful for some cloud and Hyper-V workflows.

  vhd-dynamic
      Create dynamic VHD image using qemu-img vpc subformat=dynamic.

  vhdx
      Create VHDX image with qemu-img, if supported by host qemu-img.

  proxmox
      Create Proxmox import helper bundle and ensure qcow2 image exists.

  azure
      Run existing container create-azure command.

  ova
      Run existing container create-ova command.

  ovf
      Run existing container create-ova --ovf command.

  ovf-mf
      Run existing container create-ova --ovf --mf command.

  vmdk-only
      Run existing container create-ova --vmdk-only command.

Legacy flags:
  --ovf
      Same intent as adding ovf to --formats.
  --mf
      Used with --ovf; same as ovf-mf when both are set.
  --vmdk-only
      Same intent as adding vmdk-only to --formats.

Other options:
  --src-repo-url            Remote/public repository URL. If set, local repo creation is skipped.
  --output-dir              Directory for converted/extra artifacts.
                            Default: directory containing --raw-image.
  --skip-repo-create        Do not run create-repo.
  --skip-raw-create         Do not run create-image. Useful to convert an existing raw image.
  --keep-going              Continue generating remaining formats when one conversion fails.
  --dry-run                 Print commands without executing them.

Proxmox options:
  --proxmox-vm-name <name>  VM name in generated import script. Default: ova-name or raw basename.
  --proxmox-storage <name>  Proxmox storage for qm importdisk. Default: local-lvm.
  --proxmox-bridge <name>   Proxmox network bridge. Default: vmbr0.
  --proxmox-vmid <id>       VMID in generated import script. Default: 9000.
  --proxmox-memory <MiB>    VM memory. Default: 2048.
  --proxmox-cores <n>       VM CPU cores. Default: 2.

Environment:
  NICEOS_IMAGE_NAME         Container image name. Default: niceos/installer.
  POI_IMAGE_NAME            Compatibility alias for NICEOS_IMAGE_NAME.
  NICEOS_INSTALLER_PATH     Default source path variable.
  POI_PATH                  Compatibility alias for NICEOS_INSTALLER_PATH.
  DOCKER_BIN                Docker binary. Default: docker.
  DOCKER_WITH_SUDO          auto|yes|no. Default: auto.
  QEMU_IMG_BIN              qemu-img binary. Default: qemu-img.
  RELEASE_VERSION           Default release version if --releasever is not specified.
  ARCH                      Default target arch if --arch is not specified.

Examples:

  Build only RAW/base image:
    niceos-create-image.sh \
      --raw-image niceos-minimal.raw \
      --config-file minimal_ks.yaml \
      --local-repo-path /DATA/niceos/repo/5.0 \
      --niceos-installer-path /DATA/src/niceos-os-installer \
      --flavor ova \
      --releasever 5.0 \
      --formats raw

  Build RAW + qcow2 + Proxmox helper:
    niceos-create-image.sh \
      --raw-image niceos-minimal.raw \
      --config-file minimal_ks.yaml \
      --local-repo-path /DATA/niceos/repo/5.0 \
      --niceos-installer-path /DATA/src/niceos-os-installer \
      --flavor ova \
      --ova-config minimal.yaml \
      --ova-name niceos-minimal \
      --formats raw,qcow2,proxmox \
      --output-dir /DATA/niceos/images/minimal

  Convert existing RAW into all host-side formats without rebuilding:
    niceos-create-image.sh \
      --raw-image /DATA/niceos/images/minimal/niceos-minimal.raw \
      --config-file minimal_ks.yaml \
      --local-repo-path /DATA/niceos/repo/5.0 \
      --niceos-installer-path /DATA/src/niceos-os-installer \
      --flavor ova \
      --skip-repo-create \
      --skip-raw-create \
      --formats raw.gz,raw.xz,qcow2,vmdk,vdi,vhd-fixed,vhdx,proxmox

  Build OVA and OVF+MF using existing container helper:
    niceos-create-image.sh \
      --raw-image minimal.img \
      --config-file minimal_ks.yaml \
      --local-repo-path /DATA/niceos/repo/5.0 \
      --niceos-installer-path /DATA/src/niceos-os-installer \
      --flavor ova \
      --ova-config minimal.yaml \
      --ova-name niceos-minimal \
      --formats ova,ovf-mf

  Build maximum supported artifacts:
    niceos-create-image.sh \
      --raw-image minimal.img \
      --config-file minimal_ks.yaml \
      --local-repo-path /DATA/niceos/repo/5.0 \
      --niceos-installer-path /DATA/src/niceos-os-installer \
      --flavor ova \
      --ova-config minimal.yaml \
      --ova-name niceos-minimal \
      --formats all \
      --output-dir /DATA/niceos/images/minimal \
      --keep-going

EOF
}


cleanup() {
    :
}


on_error() {
    local exit_code=$?
    local line_no=${1:-unknown}

    log "FAILED at line ${line_no}, exit code ${exit_code}"
    exit "$exit_code"
}


trap 'on_error $LINENO' ERR
trap cleanup EXIT


require_cmd() {
    local cmd="$1"

    command -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd"
}


optional_cmd_exists() {
    local cmd="$1"

    command -v "$cmd" >/dev/null 2>&1
}


is_in_array() {
    local needle="$1"
    shift

    local item
    for item in "$@"; do
        if [ "$needle" = "$item" ]; then
            return 0
        fi
    done

    return 1
}


quote_cmd() {
    local arg
    printf '%q ' "$@"
    printf '\n'
}


run_cmd() {
    log "RUN: $(quote_cmd "$@")"

    if [ "$DRY_RUN" = "true" ]; then
        return 0
    fi

    "$@"
}


run_or_handle_failure() {
    local description="$1"
    shift

    if run_cmd "$@"; then
        return 0
    fi

    if [ "$KEEP_GOING" = "true" ]; then
        warn "Skipping failed step: $description"
        return 0
    fi

    die "Failed step: $description"
}


abspath_maybe() {
    local path="$1"

    if command -v realpath >/dev/null 2>&1; then
        realpath -m "$path"
    else
        python3 - "$path" <<'PY'
import os
import sys
print(os.path.abspath(sys.argv[1]))
PY
    fi
}


path_is_under() {
    local child="$1"
    local parent="$2"

    child="$(abspath_maybe "$child")"
    parent="$(abspath_maybe "$parent")"

    case "$child" in
        "$parent"/*|"$parent")
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}


relative_to() {
    local child="$1"
    local parent="$2"

    python3 - "$child" "$parent" <<'PY'
import os
import sys
print(os.path.relpath(os.path.abspath(sys.argv[1]), os.path.abspath(sys.argv[2])))
PY
}


safe_hash() {
    local value="$1"

    if command -v sha256sum >/dev/null 2>&1; then
        printf '%s' "$value" | sha256sum | awk '{print substr($1, 1, 16)}'
    else
        printf '%s' "$value" | cksum | awk '{print $1}'
    fi
}


normalize_arch_to_docker() {
    local arch="$1"

    case "$arch" in
        x86_64|amd64)
            printf 'amd64'
            ;;
        aarch64|arm64)
            printf 'arm64'
            ;;
        *)
            return 1
            ;;
    esac
}


normalize_arch_to_uname() {
    local arch="$1"

    case "$arch" in
        x86_64|amd64)
            printf 'x86_64'
            ;;
        aarch64|arm64)
            printf 'aarch64'
            ;;
        *)
            return 1
            ;;
    esac
}


docker_prefix() {
    if [ "$DOCKER_WITH_SUDO" = "yes" ]; then
        printf '%s\0' sudo "$DOCKER_BIN"
        return 0
    fi

    if [ "$DOCKER_WITH_SUDO" = "no" ]; then
        printf '%s\0' "$DOCKER_BIN"
        return 0
    fi

    if [ "$(id -u)" -eq 0 ]; then
        printf '%s\0' "$DOCKER_BIN"
    else
        printf '%s\0' sudo "$DOCKER_BIN"
    fi
}


docker_cmd_array() {
    local -n __out="$1"
    local item

    __out=()

    while IFS= read -r -d '' item; do
        __out+=("$item")
    done < <(docker_prefix)
}


container_image_for_arch() {
    if [ "$DOCKER_ARCH" = "$SYSTEM_DOCKER_ARCH" ]; then
        printf '%s' "$NICEOS_IMAGE_NAME"
    else
        printf '%s/%s' "$DOCKER_ARCH/$NICEOS_IMAGE_NAME"
    fi
}


reset_extra_mounts() {
    EXTRA_DOCKER_MOUNTS=()
}


add_mount() {
    local host_path="$1"
    local container_path="$2"
    local mode="${3:-rw}"

    EXTRA_DOCKER_MOUNTS+=("-v" "${host_path}:${container_path}:${mode}")
}


map_input_file_to_container() {
    local path="$1"

    MAP_RESULT=""

    if [ -z "$path" ]; then
        return 0
    fi

    if [[ "$path" != /* ]]; then
        if [ ! -f "$FLAVOR_DIR/$path" ]; then
            die "Input file not found relative to flavor directory: $FLAVOR_DIR/$path"
        fi

        MAP_RESULT="$path"
        return 0
    fi

    path="$(abspath_maybe "$path")"

    if [ ! -f "$path" ]; then
        die "Input file not found: $path"
    fi

    if path_is_under "$path" "$FLAVOR_DIR"; then
        local rel
        rel="$(relative_to "$path" "$FLAVOR_DIR")"
        MAP_RESULT="/workdir/$rel"
        return 0
    fi

    local dir
    local base
    local hash
    dir="$(dirname "$path")"
    base="$(basename "$path")"
    hash="$(safe_hash "$dir")"

    add_mount "$dir" "/niceos-inputs/$hash" "ro"
    MAP_RESULT="/niceos-inputs/$hash/$base"
}


map_output_file_to_container() {
    local path="$1"

    MAP_RESULT=""

    if [ -z "$path" ]; then
        return 0
    fi

    if [[ "$path" != /* ]]; then
        MAP_RESULT="$path"
        return 0
    fi

    path="$(abspath_maybe "$path")"

    local dir
    local base
    local hash
    dir="$(dirname "$path")"
    base="$(basename "$path")"

    mkdir -p "$dir"

    if path_is_under "$path" "$FLAVOR_DIR"; then
        local rel
        rel="$(relative_to "$path" "$FLAVOR_DIR")"
        MAP_RESULT="/workdir/$rel"
        return 0
    fi

    hash="$(safe_hash "$dir")"

    add_mount "$dir" "/niceos-outputs/$hash" "rw"
    MAP_RESULT="/niceos-outputs/$hash/$base"
}


docker_image_exists() {
    local image="$1"
    local docker=()

    docker_cmd_array docker

    if [ "$DRY_RUN" = "true" ]; then
        return 1
    fi

    "${docker[@]}" image inspect "$image" >/dev/null 2>&1
}


docker_build_installer_image() {
    local image="$1"
    local docker=()
    local build_cmd=()

    docker_cmd_array docker

    build_cmd=(
        "${docker[@]}"
        buildx
        build
        --load
        --build-context
        "poi-helper=$NICEOS_INSTALLER_PATH"
        --build-context
        "niceos-helper=$NICEOS_INSTALLER_PATH"
        -t
        "$image"
    )

    if [ "$DOCKER_ARCH" != "$SYSTEM_DOCKER_ARCH" ]; then
        build_cmd+=(--platform "linux/$DOCKER_ARCH")
    fi

    build_cmd+=(.)

    run_cmd "${build_cmd[@]}"
}


docker_run_installer() {
    local image="$1"
    shift

    local docker=()
    local run_args=()

    docker_cmd_array docker

    run_args=(
        "${docker[@]}"
        run
        --rm
        --privileged
        -v
        "/dev:/dev"
        -v
        "$FLAVOR_DIR:/workdir"
        -v
        "$LOCAL_REPO_PATH:/repo"
        "${EXTRA_DOCKER_MOUNTS[@]}"
        "$image"
        "$@"
    )

    run_cmd "${run_args[@]}"
}


validate_url_like() {
    local url="$1"

    case "$url" in
        http://*|https://*|ftp://*|file://*|/*)
            return 0
            ;;
        *)
            die "Repository URL/path looks invalid: $url"
            ;;
    esac
}


split_formats() {
    local raw="$1"
    local item

    FORMATS=()

    if [ -z "$raw" ]; then
        return 0
    fi

    raw="${raw// /}"

    if [ "$raw" = "all" ]; then
        FORMATS=("${ALL_FORMATS[@]}")
        return 0
    fi

    IFS=',' read -r -a FORMATS <<< "$raw"

    for item in "${FORMATS[@]}"; do
        if [ -z "$item" ]; then
            die "Empty item in --formats"
        fi
    done
}


format_requested() {
    local format="$1"
    local item

    for item in "${FORMATS[@]}"; do
        if [ "$item" = "$format" ]; then
            return 0
        fi
    done

    return 1
}


append_format_if_missing() {
    local format="$1"

    if ! format_requested "$format"; then
        FORMATS+=("$format")
    fi
}


normalize_legacy_format_flags() {
    if [ "$CREATE_OVF" = "true" ] && [ "$CREATE_MF" = "true" ]; then
        append_format_if_missing "ovf-mf"
    elif [ "$CREATE_OVF" = "true" ]; then
        append_format_if_missing "ovf"
    fi

    if [ "$VMDK_ONLY" = "true" ]; then
        append_format_if_missing "vmdk-only"
    fi
}


validate_formats() {
    local item

    if [ "${#FORMATS[@]}" -eq 0 ]; then
        # Preserve old behavior: build raw/base image and run finalizer only
        # when legacy flags/flavor require it. No host-side conversions by default.
        return 0
    fi

    for item in "${FORMATS[@]}"; do
        if ! is_in_array "$item" "${ALL_FORMATS[@]}"; then
            die "Unsupported format '$item'. Supported: ${ALL_FORMATS[*]}"
        fi

        case "$item" in
            azure)
                if [ "$IMAGE_FLAVOR" != "azure" ]; then
                    warn "Format 'azure' is requested while --flavor is '$IMAGE_FLAVOR'. It may still work only if the container supports create-azure in this flavor directory."
                fi
                ;;
            ova|ovf|ovf-mf|vmdk-only)
                if [ -z "$OVA_CONFIG" ] || [ -z "$OVA_NAME" ]; then
                    die "Format '$item' requires --ova-config and --ova-name"
                fi
                ;;
        esac
    done
}


needs_raw_image_path() {
    local item

    if [ -n "$RAW_IMAGE" ]; then
        return 0
    fi

    for item in "${FORMATS[@]}"; do
        case "$item" in
            raw|raw.gz|raw.xz|qcow2|qcow2-compressed|vmdk|vdi|vhd-fixed|vhd-dynamic|vhdx|proxmox|azure|ova|ovf|ovf-mf|vmdk-only)
                return 1
                ;;
        esac
    done

    if [ "$IMAGE_FLAVOR" = "azure" ] || [ "$IMAGE_FLAVOR" = "ova" ]; then
        return 1
    fi

    return 0
}


resolve_raw_and_output_paths() {
    if [ -n "$RAW_IMAGE" ]; then
        if [[ "$RAW_IMAGE" = /* ]]; then
            RAW_IMAGE_HOST_PATH="$(abspath_maybe "$RAW_IMAGE")"
        else
            RAW_IMAGE_HOST_PATH="$(abspath_maybe "$FLAVOR_DIR/$RAW_IMAGE")"
        fi
    else
        RAW_IMAGE_HOST_PATH=""
    fi

    if [ -n "$OUTPUT_DIR" ]; then
        OUTPUT_DIR_ABS="$(abspath_maybe "$OUTPUT_DIR")"
    elif [ -n "$RAW_IMAGE_HOST_PATH" ]; then
        OUTPUT_DIR_ABS="$(dirname "$RAW_IMAGE_HOST_PATH")"
    else
        OUTPUT_DIR_ABS="$(abspath_maybe "$FLAVOR_DIR")"
    fi

    mkdir -p "$OUTPUT_DIR_ABS"
}


validate_args() {
    if [ -z "$IMAGE_FLAVOR" ]; then
        die "image flavor is missing"
    fi

    if ! is_in_array "$IMAGE_FLAVOR" "${SUPPORTED_FLAVORS[@]}"; then
        die "Unsupported image flavor '$IMAGE_FLAVOR'. Supported: ${SUPPORTED_FLAVORS[*]}"
    fi

    if [ -z "$KS_CONFIG_FILE" ]; then
        die "config file is missing"
    fi

    if [ -z "$LOCAL_REPO_PATH" ]; then
        die "local repo path is missing"
    fi

    if [ -z "$NICEOS_INSTALLER_PATH" ]; then
        die "NiceOS installer path is missing. Use --niceos-installer-path or --poi-path"
    fi

    if ! needs_raw_image_path; then
        die "raw image is required for selected flavor/formats"
    fi

    if [ "$IMAGE_FLAVOR" = "ova" ]; then
        if [ "$CREATE_OVF" = "true" ] && [ "$VMDK_ONLY" = "true" ]; then
            die "cannot use --ovf and --vmdk-only simultaneously"
        fi
    fi

    if [ "$IMAGE_FLAVOR" != "ova" ]; then
        if [ "$CREATE_OVF" = "true" ] || [ "$CREATE_MF" = "true" ] || [ "$VMDK_ONLY" = "true" ]; then
            die "--ovf, --mf and --vmdk-only are only valid for --flavor ova"
        fi
    fi

    if [ -n "$SRC_REPO_URL" ]; then
        validate_url_like "$SRC_REPO_URL"
    fi

    NICEOS_INSTALLER_PATH="$(abspath_maybe "$NICEOS_INSTALLER_PATH")"

    [ -d "$NICEOS_INSTALLER_PATH" ] || die "NiceOS installer path is not a directory: $NICEOS_INSTALLER_PATH"
    [ -d "$NICEOS_INSTALLER_PATH/docker" ] || die "Docker directory not found: $NICEOS_INSTALLER_PATH/docker"
    [ -d "$NICEOS_INSTALLER_PATH/examples/$IMAGE_FLAVOR" ] || die "Flavor directory not found: $NICEOS_INSTALLER_PATH/examples/$IMAGE_FLAVOR"

    LOCAL_REPO_PATH="$(abspath_maybe "$LOCAL_REPO_PATH")"
    FLAVOR_DIR="$(abspath_maybe "$NICEOS_INSTALLER_PATH/examples/$IMAGE_FLAVOR")"

    if [ -z "$RELEASE_VERSION" ]; then
        die "release version must not be empty"
    fi

    split_formats "$FORMATS_RAW"
    normalize_legacy_format_flags
    validate_formats
    resolve_raw_and_output_paths

    if [ -n "$PROXMOX_VM_NAME" ]; then
        :
    elif [ -n "$OVA_NAME" ]; then
        PROXMOX_VM_NAME="$OVA_NAME"
    elif [ -n "$RAW_IMAGE_HOST_PATH" ]; then
        PROXMOX_VM_NAME="$(basename "$RAW_IMAGE_HOST_PATH")"
        PROXMOX_VM_NAME="${PROXMOX_VM_NAME%.*}"
    else
        PROXMOX_VM_NAME="niceos"
    fi
}


init_env() {
    require_cmd "$DOCKER_BIN"
    require_cmd python3

    SYSTEM_DOCKER_ARCH="$(normalize_arch_to_docker "$SYSTEM_ARCH_RAW")" || die "Unsupported system architecture: $SYSTEM_ARCH_RAW"

    if [ -n "$ARCH" ]; then
        if ! is_in_array "$ARCH" "${SUPPORTED_ARCHES[@]}"; then
            warn "$ARCH is not valid. Falling back to system architecture: $SYSTEM_ARCH_RAW"
            ARCH="$SYSTEM_ARCH_RAW"
        fi
    else
        ARCH="$SYSTEM_ARCH_RAW"
        log "Using default system architecture: $ARCH"
    fi

    DOCKER_ARCH="$(normalize_arch_to_docker "$ARCH")" || die "Unsupported target architecture: $ARCH"
    ARCH="$(normalize_arch_to_uname "$ARCH")" || die "Unsupported target architecture: $ARCH"

    log "System architecture: $SYSTEM_ARCH_RAW -> docker/$SYSTEM_DOCKER_ARCH"
    log "Target architecture: $ARCH -> docker/$DOCKER_ARCH"

    CONTAINER_IMAGE="$(container_image_for_arch)"

    log "Using container image: $CONTAINER_IMAGE"

    cd "$NICEOS_INSTALLER_PATH/docker" || die "Failed to cd into docker directory: $NICEOS_INSTALLER_PATH/docker"

    if docker_image_exists "$CONTAINER_IMAGE"; then
        log "$CONTAINER_IMAGE container image already exists"
    else
        log "Building $CONTAINER_IMAGE container image..."
        docker_build_installer_image "$CONTAINER_IMAGE"
        log "Container image build completed"
    fi
}


create_repo_and_raw_image() {
    cd "$FLAVOR_DIR" || die "Failed to cd into flavor directory: $FLAVOR_DIR"

    mkdir -p "$LOCAL_REPO_PATH"

    log "Flavor directory: $FLAVOR_DIR"
    log "Local repo path: $LOCAL_REPO_PATH"

    if [ "$SKIP_REPO_CREATE" = "true" ]; then
        log "Skipping local repository creation by request"
    elif [ -z "$SRC_REPO_URL" ]; then
        reset_extra_mounts
        map_input_file_to_container "$KS_CONFIG_FILE"
        local ks_in_container="$MAP_RESULT"

        log "Creating local repository for release $RELEASE_VERSION"
        docker_run_installer \
            "$CONTAINER_IMAGE" \
            create-repo \
            -c "$ks_in_container" \
            -v "$RELEASE_VERSION"
        log "Local repository creation completed"
    else
        log "Using remote/public repository: $SRC_REPO_URL"
        log "Local repository creation is skipped"
    fi

    if [ "$SKIP_RAW_CREATE" = "true" ]; then
        log "Skipping raw/base image creation by request"
        return 0
    fi

    reset_extra_mounts
    map_input_file_to_container "$KS_CONFIG_FILE"
    local ks_for_image="$MAP_RESULT"

    local image_cmd=(
        create-image
        -c "$ks_for_image"
        -v "$RELEASE_VERSION"
    )

    if [ -n "$SRC_REPO_URL" ]; then
        image_cmd+=("--repo-paths=$SRC_REPO_URL")
    fi

    log "Building raw/base image"
    docker_run_installer "$CONTAINER_IMAGE" "${image_cmd[@]}"
    log "Raw/base image build completed"
}


ensure_raw_exists_for_conversion() {
    if [ "$DRY_RUN" = "true" ]; then
        return 0
    fi

    if [ -z "$RAW_IMAGE_HOST_PATH" ]; then
        die "RAW image path is empty"
    fi

    if [ ! -f "$RAW_IMAGE_HOST_PATH" ]; then
        die "RAW image was not found: $RAW_IMAGE_HOST_PATH"
    fi
}


host_artifact_name() {
    local suffix="$1"
    local base

    base="$(basename "$RAW_IMAGE_HOST_PATH")"
    base="${base%.*}"

    printf '%s/%s.%s' "$OUTPUT_DIR_ABS" "$base" "$suffix"
}


copy_raw_artifact() {
    ensure_raw_exists_for_conversion

    local dest
    dest="$(host_artifact_name "raw")"

    if [ "$(abspath_maybe "$RAW_IMAGE_HOST_PATH")" = "$(abspath_maybe "$dest")" ]; then
        log "RAW artifact already in output location: $dest"
        return 0
    fi

    run_or_handle_failure "copy raw artifact" cp -f "$RAW_IMAGE_HOST_PATH" "$dest"
}


compress_raw_gzip() {
    ensure_raw_exists_for_conversion
    require_cmd "$GZIP_BIN"

    local dest
    dest="$(host_artifact_name "raw.gz")"

    log "Creating gzip-compressed RAW: $dest"

    if [ "$DRY_RUN" = "true" ]; then
        log "RUN: $GZIP_BIN -c $RAW_IMAGE_HOST_PATH > $dest"
        return 0
    fi

    "$GZIP_BIN" -c "$RAW_IMAGE_HOST_PATH" > "$dest"
}


compress_raw_xz() {
    ensure_raw_exists_for_conversion
    require_cmd "$XZ_BIN"

    local dest
    dest="$(host_artifact_name "raw.xz")"

    log "Creating xz-compressed RAW: $dest"

    if [ "$DRY_RUN" = "true" ]; then
        log "RUN: $XZ_BIN -T0 -c $RAW_IMAGE_HOST_PATH > $dest"
        return 0
    fi

    "$XZ_BIN" -T0 -c "$RAW_IMAGE_HOST_PATH" > "$dest"
}


qemu_convert() {
    local description="$1"
    shift

    ensure_raw_exists_for_conversion
    require_cmd "$QEMU_IMG_BIN"

    run_or_handle_failure "$description" "$QEMU_IMG_BIN" convert "$@"
}


create_qcow2() {
    local dest
    dest="$(host_artifact_name "qcow2")"

    qemu_convert "qcow2 conversion" -f raw -O qcow2 "$RAW_IMAGE_HOST_PATH" "$dest"
}


create_qcow2_compressed() {
    local dest
    dest="$(host_artifact_name "qcow2.compressed")"

    qemu_convert "compressed qcow2 conversion" -f raw -O qcow2 -c "$RAW_IMAGE_HOST_PATH" "$dest"
}


create_vmdk() {
    local dest
    dest="$(host_artifact_name "vmdk")"

    qemu_convert "vmdk conversion" -f raw -O vmdk "$RAW_IMAGE_HOST_PATH" "$dest"
}


create_vdi() {
    local dest
    dest="$(host_artifact_name "vdi")"

    qemu_convert "vdi conversion" -f raw -O vdi "$RAW_IMAGE_HOST_PATH" "$dest"
}


create_vhd_fixed() {
    local dest
    dest="$(host_artifact_name "fixed.vhd")"

    qemu_convert "fixed vhd conversion" -f raw -O vpc -o subformat=fixed,force_size "$RAW_IMAGE_HOST_PATH" "$dest"
}


create_vhd_dynamic() {
    local dest
    dest="$(host_artifact_name "dynamic.vhd")"

    qemu_convert "dynamic vhd conversion" -f raw -O vpc -o subformat=dynamic "$RAW_IMAGE_HOST_PATH" "$dest"
}


create_vhdx() {
    local dest
    dest="$(host_artifact_name "vhdx")"

    qemu_convert "vhdx conversion" -f raw -O vhdx "$RAW_IMAGE_HOST_PATH" "$dest"
}


create_proxmox_bundle() {
    ensure_raw_exists_for_conversion

    local proxmox_dir
    local qcow2_path
    local import_script
    local notes_file
    local raw_base
    local disk_for_import

    proxmox_dir="$OUTPUT_DIR_ABS/proxmox"
    mkdir -p "$proxmox_dir"

    raw_base="$(basename "$RAW_IMAGE_HOST_PATH")"
    raw_base="${raw_base%.*}"

    qcow2_path="$(host_artifact_name "qcow2")"

    if [ ! -f "$qcow2_path" ] && [ "$DRY_RUN" != "true" ]; then
        log "Proxmox format requested; qcow2 does not exist yet, creating it"
        create_qcow2
    elif [ "$DRY_RUN" = "true" ]; then
        log "Proxmox format requested; would ensure qcow2 exists: $qcow2_path"
    fi

    if [ -f "$qcow2_path" ] || [ "$DRY_RUN" = "true" ]; then
        disk_for_import="$qcow2_path"
    else
        disk_for_import="$RAW_IMAGE_HOST_PATH"
    fi

    import_script="$proxmox_dir/${raw_base}-proxmox-import.sh"
    notes_file="$proxmox_dir/${raw_base}-proxmox-notes.txt"

    cat > "$import_script" <<EOF
#!/usr/bin/env bash
#
# Generated by NiceOS image helper.
#
# Run this script on a Proxmox VE node.
# Adjust VMID, STORAGE and BRIDGE before running if needed.
#

set -Eeuo pipefail

VMID="\${VMID:-$PROXMOX_VMID}"
VM_NAME="\${VM_NAME:-$PROXMOX_VM_NAME}"
STORAGE="\${STORAGE:-$PROXMOX_STORAGE}"
BRIDGE="\${BRIDGE:-$PROXMOX_BRIDGE}"
MEMORY="\${MEMORY:-$PROXMOX_MEMORY}"
CORES="\${CORES:-$PROXMOX_CORES}"
DISK_BUS="\${DISK_BUS:-$PROXMOX_DISK_BUS}"
NET_MODEL="\${NET_MODEL:-$PROXMOX_NET_MODEL}"
OS_TYPE="\${OS_TYPE:-$PROXMOX_OS_TYPE}"

IMAGE_PATH="\${IMAGE_PATH:-$disk_for_import}"

if ! command -v qm >/dev/null 2>&1; then
    echo "ERROR: qm command not found. Run this on a Proxmox VE node." >&2
    exit 1
fi

if [ ! -f "\$IMAGE_PATH" ]; then
    echo "ERROR: image not found: \$IMAGE_PATH" >&2
    exit 1
fi

echo "Creating VM \$VMID (\$VM_NAME)"
qm create "\$VMID" \\
    --name "\$VM_NAME" \\
    --memory "\$MEMORY" \\
    --cores "\$CORES" \\
    --ostype "\$OS_TYPE" \\
    --net0 "\$NET_MODEL,bridge=\$BRIDGE"

echo "Importing disk \$IMAGE_PATH to storage \$STORAGE"
qm importdisk "\$VMID" "\$IMAGE_PATH" "\$STORAGE"

echo "Attaching imported disk"
qm set "\$VMID" --scsihw virtio-scsi-pci --\${DISK_BUS}0 "\$STORAGE:vm-\${VMID}-disk-0"

echo "Setting boot order"
qm set "\$VMID" --boot order=\${DISK_BUS}0

echo "Recommended: add serial console if your image uses serial output"
qm set "\$VMID" --serial0 socket --vga serial0 || true

echo "Done."
echo "Start with: qm start \$VMID"
EOF

    chmod +x "$import_script"

    cat > "$notes_file" <<EOF
NiceOS Proxmox import notes
===========================

Generated image:
  $disk_for_import

Generated import script:
  $import_script

Run on a Proxmox VE node:

  VMID=$PROXMOX_VMID STORAGE=$PROXMOX_STORAGE BRIDGE=$PROXMOX_BRIDGE bash "$import_script"

Manual equivalent:

  qm create $PROXMOX_VMID \\
    --name "$PROXMOX_VM_NAME" \\
    --memory $PROXMOX_MEMORY \\
    --cores $PROXMOX_CORES \\
    --ostype $PROXMOX_OS_TYPE \\
    --net0 $PROXMOX_NET_MODEL,bridge=$PROXMOX_BRIDGE

  qm importdisk $PROXMOX_VMID "$disk_for_import" $PROXMOX_STORAGE

  qm set $PROXMOX_VMID --scsihw virtio-scsi-pci --${PROXMOX_DISK_BUS}0 $PROXMOX_STORAGE:vm-${PROXMOX_VMID}-disk-0

  qm set $PROXMOX_VMID --boot order=${PROXMOX_DISK_BUS}0

Notes:
  * Proxmox can import raw, qcow2 and vmdk.
  * qcow2 is generated automatically when --formats proxmox is used and qemu-img is available.
  * If your image uses cloud-init, add an ide2 cloudinit drive and configure user/network data separately.
  * If your image boots through serial console, keep serial0/vga serial0.
EOF

    log "Created Proxmox import helper: $import_script"
    log "Created Proxmox notes: $notes_file"
}


create_host_formats() {
    local item

    if [ "${#FORMATS[@]}" -eq 0 ]; then
        return 0
    fi

    for item in "${FORMATS[@]}"; do
        case "$item" in
            raw)
                copy_raw_artifact
                ;;
            raw.gz)
                compress_raw_gzip
                ;;
            raw.xz)
                compress_raw_xz
                ;;
            qcow2)
                create_qcow2
                ;;
            qcow2-compressed)
                create_qcow2_compressed
                ;;
            vmdk)
                create_vmdk
                ;;
            vdi)
                create_vdi
                ;;
            vhd-fixed)
                create_vhd_fixed
                ;;
            vhd-dynamic)
                create_vhd_dynamic
                ;;
            vhdx)
                create_vhdx
                ;;
            proxmox)
                create_proxmox_bundle
                ;;
            azure|ova|ovf|ovf-mf|vmdk-only)
                # handled separately by container finalizers
                ;;
            *)
                die "Internal error: unhandled format $item"
                ;;
        esac
    done
}


create_container_format_azure() {
    cd "$FLAVOR_DIR" || die "Failed to cd into flavor directory: $FLAVOR_DIR"

    reset_extra_mounts
    map_output_file_to_container "$RAW_IMAGE"
    local raw_image_for_container="$MAP_RESULT"

    log "Running container Azure image finalizer"
    docker_run_installer \
        "$CONTAINER_IMAGE" \
        create-azure \
        --raw-image "$raw_image_for_container"
}


create_container_format_ova_like() {
    local mode="$1"

    cd "$FLAVOR_DIR" || die "Failed to cd into flavor directory: $FLAVOR_DIR"

    reset_extra_mounts

    map_output_file_to_container "$RAW_IMAGE"
    local raw_image_for_container="$MAP_RESULT"

    map_input_file_to_container "$KS_CONFIG_FILE"
    local ks_for_ova="$MAP_RESULT"

    map_input_file_to_container "$OVA_CONFIG"
    local ova_config_for_container="$MAP_RESULT"

    local flavor_cmd=(
        create-ova
        --installer-config "$ks_for_ova"
        --ova-config "$ova_config_for_container"
        --ova-name "$OVA_NAME"
        --raw-image "$raw_image_for_container"
    )

    case "$mode" in
        ova)
            ;;
        ovf)
            flavor_cmd+=(--ovf)
            ;;
        ovf-mf)
            flavor_cmd+=(--ovf --mf)
            ;;
        vmdk-only)
            flavor_cmd+=(--vmdk-only)
            ;;
        *)
            die "Internal error: unknown OVA mode $mode"
            ;;
    esac

    log "Running container OVA/OVF/VMDK finalizer: $mode"
    docker_run_installer "$CONTAINER_IMAGE" "${flavor_cmd[@]}"
}


create_container_formats() {
    local ran_legacy_finalizer="false"

    # Preserve old behavior when no --formats was provided:
    #   --flavor azure => run create-azure
    #   --flavor ova   => run create-ova with legacy --ovf/--mf/--vmdk-only flags
    if [ "${#FORMATS[@]}" -eq 0 ]; then
        if [ "$IMAGE_FLAVOR" = "azure" ]; then
            create_container_format_azure
            return 0
        fi

        if [ "$IMAGE_FLAVOR" = "ova" ]; then
            if [ "$CREATE_OVF" = "true" ] && [ "$CREATE_MF" = "true" ]; then
                create_container_format_ova_like "ovf-mf"
            elif [ "$CREATE_OVF" = "true" ]; then
                create_container_format_ova_like "ovf"
            elif [ "$VMDK_ONLY" = "true" ]; then
                create_container_format_ova_like "vmdk-only"
            else
                create_container_format_ova_like "ova"
            fi
            return 0
        fi

        return 0
    fi

    if format_requested "azure"; then
        create_container_format_azure
    fi

    if format_requested "ova"; then
        create_container_format_ova_like "ova"
        ran_legacy_finalizer="true"
    fi

    if format_requested "ovf"; then
        create_container_format_ova_like "ovf"
        ran_legacy_finalizer="true"
    fi

    if format_requested "ovf-mf"; then
        create_container_format_ova_like "ovf-mf"
        ran_legacy_finalizer="true"
    fi

    if format_requested "vmdk-only"; then
        create_container_format_ova_like "vmdk-only"
        ran_legacy_finalizer="true"
    fi

    if [ "$ran_legacy_finalizer" = "false" ]; then
        :
    fi
}


print_summary() {
    local formats_display

    if [ "${#FORMATS[@]}" -eq 0 ]; then
        formats_display="<legacy/default>"
    else
        formats_display="${FORMATS[*]}"
    fi

    cat >&2 <<EOF

NiceOS image build summary
==========================
Release version       : $RELEASE_VERSION
Flavor                : $IMAGE_FLAVOR
Target arch           : $ARCH / docker/$DOCKER_ARCH
System arch           : $SYSTEM_ARCH_RAW / docker/$SYSTEM_DOCKER_ARCH
Container image       : $CONTAINER_IMAGE
Installer path        : $NICEOS_INSTALLER_PATH
Flavor directory      : $FLAVOR_DIR
Config file           : $KS_CONFIG_FILE
Local repo path       : $LOCAL_REPO_PATH
Remote repo URL       : ${SRC_REPO_URL:-<not set>}
Raw image             : ${RAW_IMAGE:-<not set>}
Raw image host path   : ${RAW_IMAGE_HOST_PATH:-<not set>}
Output dir            : $OUTPUT_DIR_ABS
Formats               : $formats_display
OVA config            : ${OVA_CONFIG:-<not set>}
OVA name              : ${OVA_NAME:-<not set>}
Create OVF            : $CREATE_OVF
Create manifest       : $CREATE_MF
VMDK only             : $VMDK_ONLY
Skip repo create      : $SKIP_REPO_CREATE
Skip raw create       : $SKIP_RAW_CREATE
Keep going            : $KEEP_GOING
Dry run               : $DRY_RUN

Proxmox defaults
----------------
VM name               : $PROXMOX_VM_NAME
VMID                  : $PROXMOX_VMID
Storage               : $PROXMOX_STORAGE
Bridge                : $PROXMOX_BRIDGE
Memory                : $PROXMOX_MEMORY
Cores                 : $PROXMOX_CORES
Disk bus              : $PROXMOX_DISK_BUS
Network model         : $PROXMOX_NET_MODEL
OS type               : $PROXMOX_OS_TYPE

EOF
}


parse_args() {
    if ! OPTS="$(getopt -o h --long help,raw-image:,config-file:,local-repo-path:,niceos-installer-path:,poi-path:,src-repo-url:,flavor:,ova-config:,ova-name:,arch:,ovf,mf,vmdk-only,releasever:,formats:,output-dir:,skip-repo-create,skip-raw-create,keep-going,dry-run,proxmox-vm-name:,proxmox-storage:,proxmox-bridge:,proxmox-vmid:,proxmox-memory:,proxmox-cores:,proxmox-disk-bus:,proxmox-net-model:,proxmox-os-type: -n "$SCRIPT_NAME" -- "$@")"; then
        usage
        exit 1
    fi

    eval set -- "$OPTS"

    while true; do
        case "$1" in
            -h|--help)
                usage
                exit 0
                ;;
            --raw-image)
                RAW_IMAGE="$2"
                shift 2
                ;;
            --config-file)
                KS_CONFIG_FILE="$2"
                shift 2
                ;;
            --local-repo-path)
                LOCAL_REPO_PATH="$2"
                shift 2
                ;;
            --niceos-installer-path|--poi-path)
                NICEOS_INSTALLER_PATH="$2"
                shift 2
                ;;
            --src-repo-url)
                SRC_REPO_URL="$2"
                shift 2
                ;;
            --flavor)
                IMAGE_FLAVOR="$2"
                shift 2
                ;;
            --ova-config)
                OVA_CONFIG="$2"
                shift 2
                ;;
            --ova-name)
                OVA_NAME="$2"
                shift 2
                ;;
            --arch)
                ARCH="$2"
                shift 2
                ;;
            --ovf)
                CREATE_OVF="true"
                shift
                ;;
            --mf)
                CREATE_MF="true"
                shift
                ;;
            --vmdk-only)
                VMDK_ONLY="true"
                shift
                ;;
            --releasever)
                RELEASE_VERSION="$2"
                shift 2
                ;;
            --formats)
                FORMATS_RAW="$2"
                shift 2
                ;;
            --output-dir)
                OUTPUT_DIR="$2"
                shift 2
                ;;
            --skip-repo-create)
                SKIP_REPO_CREATE="true"
                shift
                ;;
            --skip-raw-create)
                SKIP_RAW_CREATE="true"
                shift
                ;;
            --keep-going)
                KEEP_GOING="true"
                shift
                ;;
            --dry-run)
                DRY_RUN="true"
                shift
                ;;
            --proxmox-vm-name)
                PROXMOX_VM_NAME="$2"
                shift 2
                ;;
            --proxmox-storage)
                PROXMOX_STORAGE="$2"
                shift 2
                ;;
            --proxmox-bridge)
                PROXMOX_BRIDGE="$2"
                shift 2
                ;;
            --proxmox-vmid)
                PROXMOX_VMID="$2"
                shift 2
                ;;
            --proxmox-memory)
                PROXMOX_MEMORY="$2"
                shift 2
                ;;
            --proxmox-cores)
                PROXMOX_CORES="$2"
                shift 2
                ;;
            --proxmox-disk-bus)
                PROXMOX_DISK_BUS="$2"
                shift 2
                ;;
            --proxmox-net-model)
                PROXMOX_NET_MODEL="$2"
                shift 2
                ;;
            --proxmox-os-type)
                PROXMOX_OS_TYPE="$2"
                shift 2
                ;;
            --)
                shift
                break
                ;;
            *)
                die "Unhandled option: $1"
                ;;
        esac
    done

    if [ "$#" -ne 0 ]; then
        die "Unexpected positional arguments: $*"
    fi
}


main() {
    parse_args "$@"
    validate_args
    init_env
    print_summary

    create_repo_and_raw_image
    create_host_formats
    create_container_formats

    log "NiceOS image build completed successfully"
}


main "$@"