#!/usr/bin/env bash
# ============================================================================
# qcrows-pack — Create a QCrows VM container image archive
#
# Packages rootfs, initrd, kernel, metadata, menu entry, and build info
# into a self-describing .qcrows tar archive for Kata Containers.
#
# Usage:
#   qcrows-pack --rootfs rootfs.tar.gz --initrd initrd.img [options] -o image.qcrows
#   qcrows-pack --from-dir ./build-output/ [options] -o image.qcrows
#
# See: qcrows-spec.md for the full format specification.
# ============================================================================
set -euo pipefail

# ─── Version ────────────────────────────────────────────────────────────────
QCROWS_SPEC_VERSION="0.2.0"
VERSION="0.2.0"

# ─── Colors ─────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

log()   { echo -e "${GREEN}[qcrows]${NC} $*"; }
warn()  { echo -e "${YELLOW}[qcrows]${NC} WARNING: $*"; }
error() { echo -e "${RED}[qcrows]${NC} ERROR: $*" >&2; }
die()   { error "$@"; exit 1; }

# ─── Defaults ───────────────────────────────────────────────────────────────
ROOTFS=""
INITRD=""
KERNEL=""
KERNEL_CONFIG=""
BOOT_PARAMS=""
METADATA_FILE=""
MENU_FILE=""
BUILD_FILE=""
SPEC_FILE=""
FIRMWARE_DIR=""
DEVICE_TREE_DIR=""
OUTPUT=""
FROM_DIR=""
NAME=""
VERSION_IMG=""
DESCRIPTION=""
ARCH=""
HYPERVISORS=""
BUILD_SYSTEM=""
DRY_RUN=false
COMPRESS=true

# ─── Temp directory ────────────────────────────────────────────────────────
TMPDIR=""
cleanup() {
  [[ -n "$TMPDIR" && -d "$TMPDIR" ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT

# ─── Helpers ────────────────────────────────────────────────────────────────

# Return the first existing file from a list of candidate paths.
# Handles glob patterns by expanding them and taking the first match.
first_existing_file() {
  local candidate match
  for candidate in "$@"; do
    for match in $candidate; do
      if [[ -f "$match" ]]; then
        echo "$match"
        return 0
      fi
    done
  done
  return 1
}

# ─── Usage ──────────────────────────────────────────────────────────────────
usage() {
  cat <<EOF
qcrows-pack v${VERSION} — Create a QCrows VM container image

USAGE:
  qcrows-pack [OPTIONS] -o OUTPUT

INPUT MODES (one required):
  --from-dir DIR        Discover components from a build output directory
  --rootfs FILE         Path to rootfs tar archive (required unless --from-dir)

REQUIRED (unless --from-dir):
  --rootfs FILE         Path to rootfs tar.gz / tar.xz / tar.zst
  -o, --output FILE     Output .qcrows file path

OPTIONAL COMPONENTS:
  --initrd FILE         Path to initrd/initramfs/cramfs image
  --kernel FILE         Path to guest kernel (vmlinuz or vmlinux)
  --kernel-config FILE  Path to kernel .config file
  --boot-params FILE    Path to boot-params.conf
  --firmware DIR        Directory of firmware blobs to include
  --device-tree DIR     Directory of .dtb files to include

METADATA (generated if not provided):
  --metadata FILE       Path to metadata.toml (overrides generation)
  --menu FILE           Path to menu.toml (overrides generation)
  --build FILE          Path to build.toml (overrides generation)
  --spec FILE           Path to spec.md build guide

IMAGE METADATA (used for generated metadata.toml):
  --name NAME           Image name (e.g., "alpine-3.20-kata")
  --version VER         Image version (e.g., "3.20.1")
  --description DESC    One-line description
  --arch ARCH           Target architecture (default: detected)
  --hypervisors LIST    Comma-separated hypervisor list (default: "qemu,cloud-hypervisor")
  --build-system SYS    Build system: gentoo|sourcemage|buildroot|lunar|lede|btc|sorcery|custom

OPTIONS:
  --no-compress         Do not gzip the output tar archive
  --dry-run             Show what would be included without creating the archive
  -h, --help            Show this help
  -v, --version         Show version

BUILD SYSTEM DETECTION (--from-dir):
  When using --from-dir, qcrows-pack detects the build system
  from the directory structure:
    - Buildroot: .config with BR2_* symbols
    - LEDE/OpenWrt: .config with CONFIG_TARGET_* symbols
    - Gentoo: /etc/portage/ in the rootfs
    - Source Mage: /var/state/sorcery/ in the rootfs
    - Lunar: /var/state/lunar/ in the rootfs

EXAMPLES:
  # From individual files
  qcrows-pack \\
    --rootfs ./output/rootfs.tar.gz \\
    --initrd ./output/initrd.img \\
    --kernel ./output/bzImage \\
    --kernel-config ./output/.config \\
    --name "alpine-3.20-kata" --version "3.20.1" \\
    --build-system buildroot \\
    --hypervisors "qemu,cloud-hypervisor" \\
    -o alpine-3.20-kata.qcrows

  # From a Buildroot output directory
  qcrows-pack --from-dir ./buildroot/output/ \\
    --name "buildroot-kata" --version "2024.02" \\
    --build-system buildroot \\
    -o buildroot-kata.qcrows

  # With pre-written metadata files
  qcrows-pack \\
    --rootfs ./rootfs.tar.gz --initrd ./initrd.img \\
    --metadata ./metadata.toml --menu ./menu.toml --build ./build.toml \\
    -o custom-kata.qcrows
EOF
}

# ─── Parse Arguments ────────────────────────────────────────────────────────
parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --rootfs)        ROOTFS="$2"; shift 2 ;;
      --initrd)        INITRD="$2"; shift 2 ;;
      --kernel)        KERNEL="$2"; shift 2 ;;
      --kernel-config) KERNEL_CONFIG="$2"; shift 2 ;;
      --boot-params)   BOOT_PARAMS="$2"; shift 2 ;;
      --firmware)      FIRMWARE_DIR="$2"; shift 2 ;;
      --device-tree)   DEVICE_TREE_DIR="$2"; shift 2 ;;
      --metadata)      METADATA_FILE="$2"; shift 2 ;;
      --menu)          MENU_FILE="$2"; shift 2 ;;
      --build)         BUILD_FILE="$2"; shift 2 ;;
      --spec)          SPEC_FILE="$2"; shift 2 ;;
      --from-dir)      FROM_DIR="$2"; shift 2 ;;
      --name)          NAME="$2"; shift 2 ;;
      --version)       VERSION_IMG="$2"; shift 2 ;;
      --description)   DESCRIPTION="$2"; shift 2 ;;
      --arch)          ARCH="$2"; shift 2 ;;
      --hypervisors)   HYPERVISORS="$2"; shift 2 ;;
      --build-system)  BUILD_SYSTEM="$2"; shift 2 ;;
      --output|-o)     OUTPUT="$2"; shift 2 ;;
      --no-compress)   COMPRESS=false; shift ;;
      --dry-run)       DRY_RUN=true; shift ;;
      --help|-h)       usage; exit 0 ;;
      --version|-v)    echo "qcrows-pack v${VERSION}"; exit 0 ;;
      *)               die "Unknown argument: $1 (use --help)" ;;
    esac
  done
}

# ─── Validation (array-driven) ──────────────────────────────────────────────
validate_inputs() {
  [[ -z "$OUTPUT" ]] && die "Output file is required (--output or -o)"
  [[ -z "$FROM_DIR" && -z "$ROOTFS" ]] && die "Either --rootfs or --from-dir is required"
  [[ -z "$FROM_DIR" && -z "$KERNEL" ]] && die "--kernel is required (QCrows v0.2+ requires a bundled kernel). Use --from-dir for discovery."
  [[ -n "$FROM_DIR" && ! -d "$FROM_DIR" ]] && die "Directory not found: $FROM_DIR"

  # Array-driven file existence checks: "variable_path:label"
  local -a file_checks=(
    "ROOTFS:rootfs"
    "INITRD:initrd"
    "KERNEL:kernel"
    "KERNEL_CONFIG:kernel config"
    "BOOT_PARAMS:boot params"
    "METADATA_FILE:metadata"
    "MENU_FILE:menu"
    "BUILD_FILE:build"
    "SPEC_FILE:spec"
  )

  local entry var_path label val
  for entry in "${file_checks[@]}"; do
    var_path="${entry%%:*}"
    label="${entry##*:}"
    val="${!var_path:-}"
    [[ -n "$val" && ! -f "$val" ]] && die "${label} file not found: ${val}"
  done

  # QCrows v0.2: kernel config is required when kernel is provided
  if [[ -z "$FROM_DIR" && -n "$KERNEL" && -z "$KERNEL_CONFIG" ]]; then
    warn "No --kernel-config provided — kernel/config is required in QCrows v0.2. Will attempt discovery."
  fi
}

# ─── Discovery from build directory ─────────────────────────────────────────
discover_from_dir() {
  local dir="$1"
  log "Discovering components from: ${dir}"

  # Rootfs — first match from candidate paths
  ROOTFS="$(first_existing_file \
    "${dir}/images/rootfs.tar.gz" \
    "${dir}/images/rootfs.tar.xz" \
    "${dir}/images/rootfs.cpio.gz" \
    "${dir}/rootfs.tar.gz" \
    "${dir}/rootfs.tar.xz" \
    "${dir}/rootfs.cpio.gz" \
    "${dir}/target/rootfs.tar.gz" \
  || true)"
  [[ -n "$ROOTFS" ]] && log "Found rootfs: ${ROOTFS}" || die "Could not find rootfs in ${dir}. Use --rootfs to specify manually."

  # Initrd
  INITRD="$(first_existing_file \
    "${dir}/images/initrd.img" \
    "${dir}/images/initramfs.cpio.gz" \
    "${dir}/initrd.img" \
    "${dir}/initramfs.cpio.gz" \
    "${dir}/images/rootfs.cpio.gz" \
    "${dir}/rootfs.cpio.gz" \
  || true)"
  [[ -n "$INITRD" ]] && log "Found initrd: ${INITRD}"

  # Kernel
  KERNEL="$(first_existing_file \
    "${dir}/images/bzImage" \
    "${dir}/images/vmlinuz" \
    "${dir}/images/vmlinux" \
    "${dir}/bzImage" \
    "${dir}/vmlinuz" \
    "${dir}/vmlinux" \
    "${dir}/boot/bzImage" \
    "${dir}/boot/vmlinuz"* \
  || true)"
  [[ -n "$KERNEL" ]] && log "Found kernel: ${KERNEL}" || die "Could not find kernel in ${dir}. QCrows v0.2 requires a bundled kernel. Use --kernel to specify manually."

  # Kernel config — includes glob patterns for build subdirectories
  KERNEL_CONFIG="$(first_existing_file \
    "${dir}/.config" \
    "${dir}/images/.config" \
    "${dir}/build/linux-*/.config" \
    "${dir}/linux/.config" \
  || true)"
  [[ -n "$KERNEL_CONFIG" ]] && log "Found kernel config: ${KERNEL_CONFIG}" || warn "No kernel .config found — kernel/config is required in QCrows v0.2"

  # Boot params
  if [[ -f "${dir}/boot-params.conf" ]]; then
    BOOT_PARAMS="${dir}/boot-params.conf"
    log "Found boot params: ${BOOT_PARAMS}"
  fi

  # Detect build system if not specified
  [[ -z "$BUILD_SYSTEM" ]] && detect_build_system "$dir"
}

# ─── Build System Detection (table-driven) ──────────────────────────────────
detect_build_system() {
  local dir="$1"

  # Detection table: method | file | token | system | label
  local -a detect_methods=("config_file" "config_file" "rootfs_tar" "rootfs_tar" "rootfs_tar")
  local -a detect_files=("${dir}/.config" "${dir}/.config" "$ROOTFS" "$ROOTFS" "$ROOTFS")
  local -a detect_tokens=("BR2_" "CONFIG_TARGET_" "etc/portage" "var/state/sorcery" "var/state/lunar")
  local -a detect_systems=("buildroot" "lede" "gentoo" "sourcemage" "lunar")
  local -a detect_labels=("Buildroot" "LEDE/OpenWrt" "Gentoo (portage in rootfs)" "Source Mage (sorcery in rootfs)" "Lunar Linux (lunar in rootfs)")

  local i method file token system label
  for i in "${!detect_methods[@]}"; do
    method="${detect_methods[$i]}"
    file="${detect_files[$i]}"
    token="${detect_tokens[$i]}"
    system="${detect_systems[$i]}"
    label="${detect_labels[$i]}"

    case "$method" in
      config_file)
        [[ -f "$file" ]] || continue
        grep -q "$token" "$file" 2>/dev/null || continue
        ;;
      rootfs_tar)
        [[ -n "$file" && -f "$file" ]] || continue
        tar -tzf "$file" 2>/dev/null | grep -q "$token" || continue
        ;;
    esac

    BUILD_SYSTEM="$system"
    log "Detected build system: ${label}"
    return 0
  done

  BUILD_SYSTEM="custom"
  warn "Could not detect build system. Use --build-system to specify."
}

# ─── Architecture detection ─────────────────────────────────────────────────
detect_arch() {
  [[ -n "$ARCH" ]] && return

  local host_arch
  host_arch="$(uname -m)"
  case "$host_arch" in
    x86_64|amd64)   ARCH="x86_64" ;;
    aarch64|arm64)  ARCH="aarch64" ;;
    riscv64)        ARCH="riscv64" ;;
    s390x)          ARCH="s390x" ;;
    ppc64le)        ARCH="ppc64le" ;;
    *)              ARCH="$host_arch" ;;
  esac
  log "Detected architecture: ${ARCH}"
}

# ─── Type detection helpers ─────────────────────────────────────────────────
detect_rootfs_type() {
  case "$1" in
    *.tar.gz|*.tgz)   echo "tar-gzip" ;;
    *.tar.xz|*.txz)   echo "tar-xz" ;;
    *.tar.zst)         echo "tar-zst" ;;
    *)                 echo "tar-gzip" ;;
  esac
}

detect_initrd_type() {
  case "$1" in
    *.cpio.gz|*.img)   echo "cpio-gzip" ;;
    *.cpio.lz4)        echo "cpio-lz4" ;;
    *.cpio.xz)         echo "cpio-xz" ;;
    *.cramfs)          echo "cramfs" ;;
    *)
      if file "$1" 2>/dev/null | grep -q "gzip"; then echo "cpio-gzip"
      elif file "$1" 2>/dev/null | grep -q "LZ4"; then echo "cpio-lz4"
      elif file "$1" 2>/dev/null | grep -q "XZ"; then echo "cpio-xz"
      else echo "cpio-gzip"
      fi
      ;;
  esac
}

detect_kernel_version() {
  local kernel_file="$1"
  [[ -z "$kernel_file" || ! -f "$kernel_file" ]] && { echo "unknown"; return; }

  # Extract version string from the kernel binary
  local ver
  ver=$(strings "$kernel_file" 2>/dev/null | grep -oP '^\d+\.\d+\.\d+' | head -1 || true)
  [[ -n "$ver" ]] && { echo "$ver"; return; }

  # Fallback: extract from kernel config
  if [[ -n "$KERNEL_CONFIG" && -f "$KERNEL_CONFIG" ]]; then
    local kv
    kv=$(grep "^CONFIG_VERSION_SIGNATURE=" "$KERNEL_CONFIG" 2>/dev/null | head -1 | grep -oP '\d+\.\d+\.\d+' | head -1 || true)
    [[ -n "$kv" ]] && { echo "$kv"; return; }
  fi

  echo "unknown"
}

# ─── Generate metadata.toml ────────────────────────────────────────────────
generate_metadata() {
  local rootfs_name
  rootfs_name="$(basename "$ROOTFS")"
  local rootfs_type
  rootfs_type="$(detect_rootfs_type "$ROOTFS")"
  local rootfs_size
  rootfs_size=$(( $(stat --format="%s" "$ROOTFS" 2>/dev/null || stat -f "%z" "$ROOTFS" 2>/dev/null || echo 0) / (1024 * 1024) ))

  local initrd_included="false"
  local initrd_type=""
  local initrd_path=""
  if [[ -n "$INITRD" ]]; then
    initrd_included="true"
    initrd_type="$(detect_initrd_type "$INITRD")"
    initrd_path="initrd$(echo "$INITRD" | grep -oE '\.[^.]*$' || echo ".img")"
  fi

  local kernel_included="false"
  local kernel_version="unknown"
  local kernel_path=""
  local kernel_format="vmlinuz"
  local kernel_size_mb=0
  if [[ -n "$KERNEL" ]]; then
    kernel_included="true"
    kernel_version="$(detect_kernel_version "$KERNEL")"
    # Normalize kernel format based on filename
    case "$(basename "$KERNEL")" in
      vmlinux*)  kernel_format="vmlinux"; kernel_path="kernel/vmlinux" ;;
      *)         kernel_format="vmlinuz"; kernel_path="kernel/vmlinuz" ;;
    esac
    kernel_size_mb=$(( $(stat --format="%s" "$KERNEL" 2>/dev/null || stat -f "%z" "$KERNEL" 2>/dev/null || echo 0) / (1024 * 1024) ))
  fi

  local img_name="${NAME:-$(basename "$ROOTFS" | sed 's/\.(tar\.[a-z]*\|tgz)$//')}"
  local img_version="${VERSION_IMG:-1.0.0}"
  local img_desc="${DESCRIPTION:-QCrows VM container image built with ${BUILD_SYSTEM}}"
  local hypervisor_list="${HYPERVISORS:-qemu,cloud-hypervisor}"

  # Convert comma-separated hypervisors to TOML array
  local hypervisor_toml
  hypervisor_toml=$(echo "$hypervisor_list" | tr ',' '\n' | sed 's/^/"/;s/$/",/' | tr '\n' ' ' | sed 's/, *$//')

  cat <<EOF
[ qcrows ]
format_version = "${QCROWS_SPEC_VERSION}"

[ image ]
name = "${img_name}"
version = "${img_version}"
description = "${img_desc}"
arch = "${ARCH}"
os = "linux"
created_at = "$(date -u +%Y-%m-%dT%H:%M:%SZ)"

[ image.compatibility ]
hypervisors = [ ${hypervisor_toml} ]
kata_runtime_min = "2.5"

[ image.resources ]
min_vcpus = 1
min_memory_mb = 256
recommended_vcpus = 2
recommended_memory_mb = 1024

[ kernel ]
version = "${kernel_version}"
included = ${kernel_included}
path = "${kernel_path}"
format = "${kernel_format}"
config_path = "kernel/config"
size_mb = ${kernel_size_mb}

[ initrd ]
included = ${initrd_included}
type = "${initrd_type}"
path = "${initrd_path}"

[ rootfs ]
type = "${rootfs_type}"
path = "rootfs${rootfs_name##rootfs}"
size_mb = ${rootfs_size}

[ agent ]
name = "kata-agent"
protocol = "vsock"

[ boot_params ]
included = $([ -n "$BOOT_PARAMS" ] && echo true || echo false)
path = "boot-params.conf"
EOF
}

# ─── Generate menu.toml ────────────────────────────────────────────────────
generate_menu() {
  local img_name="${NAME:-custom-kata}"
  local category="custom"

  # Determine category from build system
  case "$BUILD_SYSTEM" in
    buildroot|lede) category="embedded" ;;
    gentoo)         category="hardened" ;;
    sourcemage|sorcery|lunar) category="minimal" ;;
  esac

  # Override category from image name
  case "$img_name" in
    *minimal*|*alpine*|*busybox*) category="minimal" ;;
    *server*|*ubuntu*|*debian*)   category="server" ;;
    *dev*|*debug*)                category="development" ;;
    *hardened*|*secure*)          category="hardened" ;;
    *embedded*|*lede*|*openwrt*)  category="embedded" ;;
  esac

  cat <<EOF
[ menu ]
label = "${img_name}"
category = "${category}"
icon = "box"
priority = 50

[ menu.details ]
init_system = "auto"
package_count = 0
shell = "/bin/sh"

[ menu.tags ]
[ "suitable-for" ]
workloads = []
environments = []

[ menu.actions ]
import = true
deploy = false
edit_config = true
EOF
}

# ─── Generate build.toml ──────────────────────────────────────────────────
generate_build() {
  cat <<EOF
[ build ]
system = "${BUILD_SYSTEM}"
timestamp = "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
host_arch = "$(uname -m)"
target_arch = "${ARCH}"

[ build.options ]
# Add build-system-specific options here
# See qcrows-spec.md Section 2.3 (build.toml) for examples

[ build.sources ]
# List source URLs and hashes for reproducibility

[ build.reproducibility ]
reproducible = false
build_id = ""
EOF
}

# ─── Compute SHA-256 hashes ───────────────────────────────────────────────
compute_hashes() {
  local staging_dir="$1"
  (
    cd "$staging_dir"
    find . -type f ! -name "hashes.sha256" -print0 | sort -z | xargs -0 sha256sum
  )
}

# ─── Main Pack Logic ───────────────────────────────────────────────────────
do_pack() {
  TMPDIR="$(mktemp -d /tmp/qcrows-pack-XXXXXX)"
  local staging="${TMPDIR}/staging"
  mkdir -p "$staging"

  log "Staging directory: ${staging}"

  # ── 1. Copy rootfs ──────────────────────────────────────────────────
  local rootfs_dest
  rootfs_dest="rootfs$(echo "$ROOTFS" | grep -oE '\.(tar\.[a-z]+|tgz)$' || echo ".tar.gz")"
  cp "$ROOTFS" "${staging}/${rootfs_dest}"
  log "Added rootfs: $(du -sh "${staging}/${rootfs_dest}" | cut -f1)"

  # ── 2. Copy initrd ──────────────────────────────────────────────────
  if [[ -n "$INITRD" ]]; then
    local initrd_dest
    initrd_dest="initrd$(echo "$INITRD" | grep -oE '\.[^.]*$' || echo ".img")"
    cp "$INITRD" "${staging}/${initrd_dest}"
    log "Added initrd: $(du -sh "${staging}/${initrd_dest}" | cut -f1)"
  fi

  # ── 3. Copy kernel ──────────────────────────────────────────────────
  if [[ -n "$KERNEL" ]]; then
    mkdir -p "${staging}/kernel"
    # Normalize name to vmlinuz or vmlinux
    case "$(basename "$KERNEL")" in
      bzImage|vmlinuz*) cp "$KERNEL" "${staging}/kernel/vmlinuz" ;;
      vmlinux*)         cp "$KERNEL" "${staging}/kernel/vmlinux" ;;
      *)                cp "$KERNEL" "${staging}/kernel/$(basename "$KERNEL")" ;;
    esac
    log "Added kernel: $(basename "$KERNEL")"
  fi

  # ── 4. Copy kernel config ───────────────────────────────────────────
  if [[ -n "$KERNEL_CONFIG" ]]; then
    mkdir -p "${staging}/kernel"
    cp "$KERNEL_CONFIG" "${staging}/kernel/config"
    log "Added kernel config"
  fi

  # ── 5. Copy boot-params ─────────────────────────────────────────────
  [[ -n "$BOOT_PARAMS" ]] && { cp "$BOOT_PARAMS" "${staging}/boot-params.conf"; log "Added boot-params.conf"; }

  # ── 6. Copy firmware ────────────────────────────────────────────────
  if [[ -n "$FIRMWARE_DIR" && -d "$FIRMWARE_DIR" ]]; then
    mkdir -p "${staging}/firmware"
    cp -r "${FIRMWARE_DIR}/"* "${staging}/firmware/" 2>/dev/null || true
    log "Added firmware directory"
  fi

  # ── 7. Copy device-tree ─────────────────────────────────────────────
  if [[ -n "$DEVICE_TREE_DIR" && -d "$DEVICE_TREE_DIR" ]]; then
    mkdir -p "${staging}/device-tree"
    cp -r "${DEVICE_TREE_DIR}/"* "${staging}/device-tree/" 2>/dev/null || true
    log "Added device-tree directory"
  fi

  # ── 8. Generate or copy metadata.toml ───────────────────────────────
  if [[ -n "$METADATA_FILE" ]]; then
    cp "$METADATA_FILE" "${staging}/metadata.toml"
    log "Using provided metadata.toml"
  else
    generate_metadata > "${staging}/metadata.toml"
    log "Generated metadata.toml"
  fi

  # ── 9. Generate or copy menu.toml ───────────────────────────────────
  if [[ -n "$MENU_FILE" ]]; then
    cp "$MENU_FILE" "${staging}/menu.toml"
    log "Using provided menu.toml"
  else
    generate_menu > "${staging}/menu.toml"
    log "Generated menu.toml"
  fi

  # ── 10. Generate or copy build.toml ─────────────────────────────────
  if [[ -n "$BUILD_FILE" ]]; then
    cp "$BUILD_FILE" "${staging}/build.toml"
    log "Using provided build.toml"
  else
    generate_build > "${staging}/build.toml"
    log "Generated build.toml"
  fi

  # ── 11. Copy spec.md ────────────────────────────────────────────────
  [[ -n "$SPEC_FILE" ]] && { cp "$SPEC_FILE" "${staging}/spec.md"; log "Added spec.md"; }

  # ── 12. Compute hashes ──────────────────────────────────────────────
  compute_hashes "$staging" > "${staging}/hashes.sha256"
  log "Generated hashes.sha256"

  # ── Dry run — direct pipeline, no subshell loop ─────────────────────
  if [[ "$DRY_RUN" == "true" ]]; then
    echo ""
    log "=== DRY RUN — Would include the following files ==="
    (cd "$staging" && find . -type f -print0 | sort -z | xargs -0 -I{} du -h "{}" | sed "s|\t\./|  |")
    echo ""
    log "Total size: $(du -sh "$staging" | cut -f1)"
    return
  fi

  # ── 13. Create archive ─────────────────────────────────────────────
  local output_file="$OUTPUT"
  if [[ "$COMPRESS" == "true" ]]; then
    [[ ! "$output_file" == *.gz ]] && output_file="${output_file}.gz"
    log "Creating compressed archive: ${output_file}"
    (cd "$staging" && tar czf - ./*) > "$output_file"
  else
    log "Creating uncompressed archive: ${output_file}"
    (cd "$staging" && tar cf - ./*) > "$output_file"
  fi

  local final_size
  final_size="$(du -sh "$output_file" | cut -f1)"
  log "Archive created: ${output_file} (${final_size})"

  # ── 14. Summary ─────────────────────────────────────────────────────
  echo ""
  echo -e "${GREEN}╔══════════════════════════════════════════════╗${NC}"
  echo -e "${GREEN}║   QCrows image packed successfully          ║${NC}"
  echo -e "${GREEN}╚══════════════════════════════════════════════╝${NC}"
  echo ""
  echo -e "  Output:     ${CYAN}${output_file}${NC}"
  echo -e "  Size:       ${CYAN}${final_size}${NC}"
  echo -e "  Components: ${CYAN}$(find "$staging" -type f ! -name hashes.sha256 | wc -l) files${NC}"
  echo -e "  Build:      ${CYAN}${BUILD_SYSTEM}${NC}"
  echo -e "  Arch:       ${CYAN}${ARCH}${NC}"
  echo ""
  echo -e "  Verify:     ${CYAN}qcrows-verify ${output_file}${NC}"
  echo -e "  Inspect:    ${CYAN}qcrows-inspect ${output_file}${NC}"
  echo -e "  Export:     ${CYAN}qcrows-export --format qcow2 ${output_file} -o disk.qcow2${NC}"
  echo -e "              ${CYAN}qcrows-export --format iso ${output_file} -o live.iso${NC}"
  echo ""
}

# ─── Entry Point ────────────────────────────────────────────────────────────
parse_args "$@"
validate_inputs
detect_arch

# Discover from directory if specified
[[ -n "$FROM_DIR" ]] && discover_from_dir "$FROM_DIR"

do_pack
