#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# build-classic.sh — Classic Amiga platform build script
#
# Creates bootable CF/SD card images for real Amiga hardware
# (A500, A1200, A2000, A3000, A4000).
# Image layout: Pure RDB with Amiga partitions (no FAT boot partition —
# Kickstart is in the ROM socket).
# Supports optional RTG graphics cards (Picasso IV, CyberVision 64, etc.)
# and optional accelerator cards.
#
# Sources shared functions from lib/common.sh and partition_layout.sh.
# ---------------------------------------------------------------------------

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/partition_layout.sh"

SCRIPT_VERSION="${AMIGA_IMAGER_APP_VERSION:-dev}"
echo "==> Amiga-Imager Classic Amiga build script version: ${SCRIPT_VERSION}" >&2

# Safety: this script requires bash (arrays are used).
if [[ -z "${BASH_VERSION:-}" ]]; then
  echo "ERROR: This script must be run with bash (e.g. 'bash build-classic.sh ...')." >&2
  exit 2
fi
if [[ ${BASH_VERSINFO[0]} -lt 3 ]]; then
  echo "ERROR: bash 3.x+ required (macOS /bin/bash is fine)." >&2
  exit 2
fi

# Maximum parallel ADF extractions (tunable via environment; default 4)
MAX_PARALLEL_ADF="${MAX_PARALLEL_ADF:-4}"
if ! [[ "$MAX_PARALLEL_ADF" =~ ^[1-9][0-9]*$ ]]; then
  echo "==> WARN: MAX_PARALLEL_ADF='$MAX_PARALLEL_ADF' is not a valid positive integer; using default 4" >&2
  MAX_PARALLEL_ADF=4
fi

# ---------------------------------------------------------------------------
# Classic Amiga build pipeline
# ---------------------------------------------------------------------------
# - Pure RDB image (no MBR, no FAT boot partition — Kickstart in ROM)
# - Optional RTG via Picasso96 (Picasso IV, CyberVision 64, etc.)
# - Optional network stack (Roadshow)
# - No Emu68 boot bundle, no config.txt, no cmdline.txt
# - No genet/wifipi (Classic uses different NICs)
# - No Unicam
# ---------------------------------------------------------------------------

echo "==> USING SCRIPT: $0"

IMG="${1:?Output .hdf path required}"
KICK_ROM="${2:?Kickstart ROM path required}"

# Positional arguments (same format as PiStorm):
#   <img> <kick.rom> [<iso>] <assets_dir> [flags...]
ISO_PATH=""
ASSETS_DIR=""
if [[ $# -ge 4 && "${4#--}" == "$4" ]]; then
  ISO_PATH="${3:-}"
  ASSETS_DIR="${4:?Assets folder path required}"
  shift 4 || true
else
  ASSETS_DIR="${3:?Assets folder path required}"
  shift 3 || true
fi

# Treat "-" / "none" as explicit "no ISO".
case "$(printf "%s" "${ISO_PATH:-}" | tr '[:upper:]' '[:lower:]')" in
  ""|"-"|"none"|"null") ISO_PATH="" ;;
esac

# -----------------------------------------------------------------------------
# Defaults (GUI should override)
# -----------------------------------------------------------------------------
TOTAL_GB=2
DH0_MIB=0                # 0 = use all available space
DH1_MIB=0
AMIGA_FS="Ffs"            # Ffs | FfsDC | Pfs3
PFS3_AIO=""
MAX_TRANSFER="0x1fe00"
MASK="0x7ffffffe"
TRANSFER_DIR=""
TRANSFER_PARTITION=""
PARTITION_CONTENT_MAP=""
ADF_DIR=""                 # Optional: merged ADF folder
ICON_SET="standard"        # standard | glowicons
BUILD_MANIFEST=""          # optional: path to JSON manifest written by Swift (Stage D)
BUILD_PLAN=""              # optional: path to compiled shell execution plan (Phase 2A/2B)
PACKAGES=""                # comma-separated package ids resolved by Swift
LOCALES=""                 # comma-separated locale codes
CSV_DIR=""                 # explicit InputFiles directory
BLANK_AMIGA_PARTITIONS=0   # 1 = create formatted empty Amiga partitions, skip OS copy/install
AMINET_DIR=""              # staging dir with pre-downloaded .lha files from Aminet
AMINET_RULES_CSV=""        # optional CSV override for Aminet install rules
AMINET_RULES_CSV_RESOLVED=""
IBROWSE_DIR=""             # optional staging dir for IBrowse installer
IBROWSE_KEY=""             # optional IBrowse key file
GOVD_KEY=""                # optional GoVD registration key file
INCLUDE_IBROWSE=0          # 1=install IBrowse from staging dir
INCLUDE_WBDOCK=0           # 1=install WBDock2
INCLUDE_BASS=0             # 1=download and install Beneath a Steel Sky freeware
INCLUDE_FOTAQ=0            # 1=download and install Flight of the Amazon Queen freeware
INCLUDE_LURE=0             # 1=download and install Lure of the Temptress freeware
INCLUDE_GOVD=0             # 1=install GoVD virtual desktop
WHDLOAD_KICKSTARTS_DIR=""  # optional folder with Kickstart ROM files for WHDLoad
IMP3_REG=""                # optional IMP3 registration code
IMP3_REG_FILE=""           # optional path to imp3.reg file
BOING_BAG_1=""             # optional: path to Boing Bag 1 .lha
BOING_BAG_2=""             # optional: path to Boing Bag 2 .lha
APPLY_ICON_POSITIONS="1"   # 1|0 apply icon positions from CSV at build time
ICON_PASS2_MODE="scoped"   # scoped|global for the late optional-package icon pass
DETECTED_WB_VERSION="3.2"  # auto-detected from ADF filename
BACKDROP_ENABLED="1"       # WBConfig.prefs backdrop: "1"=enabled, "0"=disabled
# Classic-specific: hardware
CLASSIC_MACHINE=""         # A500 | A600 | A1200 | A2000 | A3000 | A4000 | A4000T | CD32
ACCELERATOR=""             # none | 68030 | 68040 | 68060
RTG_CARD=""                # none | PicassoIV | CyberVision64 | etc.
RTG_CARD_DRIVER=""         # P96 .card driver filename
# Classic-specific: screen mode / RTG
SCREEN_WIDTH=640
SCREEN_HEIGHT=480
SCREEN_FPS=60
SCREEN_INTERLACE=0
SCREEN_MODE_ID=""          # hex ModeID (empty = native chipset, no RTG patching)
WB_DEPTH=8
WB_DEPTH_HEX=""
P96_ICOMP_ARCHIVE=""       # optional: path to iComp Picasso96 LHA
# Classic-specific: networking
NETWORK_CARD=""            # none | Ariadne | AriadneII | A2065 | etc.
NETWORK_CARD_DEVICE=""     # SANA-II device name (ariadne.device, etc.)
NETWORK_STACK="none"       # none | roadshowDemo | roadshowFull
ROADSHOW_VARIANT=""        # demo | full
ROADSHOW_ARCHIVE=""        # path to Roadshow .lha
A314_RELEASE_TAG="v1.2.3"
A314_AMIGA_ZIP_URL="https://github.com/niklasekstrom/a314/releases/download/${A314_RELEASE_TAG}/a314-amiga.zip"
A314_AMIGA_IP="192.168.2.2"
A314_AMIGA_NETMASK="255.255.255.0"
A314_AMIGA_GATEWAY="192.168.2.1"
A314_AMIGA_DNS="8.8.8.8"
A314_BOOT_DELAY_SECS=15
A314_CONFIG_CLOCKPORT_ADDRESS=""
A314_CONFIG_INTERRUPT=""
A314_PIDRIVE_AUTOMOUNT=1
WIFI_SSID=""               # not used for Classic (no wifipi)
WIFI_PASSWORD=""
DEBUG=0
DEBUG_EXPORT_DIR=""

# N-partition support
PART_NAMES=()
PART_LABELS=()
PART_SIZES=()       # numeric MiB or "remaining"
PART_FSTYPES=()     # Ffs | FfsIntl | FfsDC | Pfs3
PART_BLOCKSIZES=()  # bytes per filesystem block
PART_BOOTABLE=()    # "1" or "0"
PART_BOOTPRI=()     # integer

# Selected icon ADFs (resolved during ADF classification)
SELECTED_ICONSET_ADFS=()

DEFAULT_HST="$HOME/Library/Application Support/Amiga-Imager/Tools/hst-imager"
HST_IMAGER="${HST_IMAGER:-$DEFAULT_HST}"
AMIGA_TOOLS="${AMIGA_TOOLS:-}"


# ===========================================================================
# Classic-specific functions
# ===========================================================================

relocate_iprefs_after_loadwb() {
  local sys_root="$1"

  local tmp_ss="$STAGE/Startup-Sequence.iprefs-relocate"
  if ! fs_pull_file "$sys_root/S/Startup-Sequence" "$tmp_ss"; then
    return 0
  fi

  if grep -q 'Amiga Imager: IPrefs relocated after LoadWB' "$tmp_ss" 2>/dev/null; then
    echo "==> IPrefs relocation: already patched, skipping"
    return 0
  fi

  local iprefs_line
  iprefs_line=$(grep -inE '^[[:space:]]*(Run[[:space:]]+>NIL:[[:space:]]+)?(C:)?IPrefs[[:space:]]*$' \
    "$tmp_ss" | head -1 | cut -d: -f1 || true)

  if [[ -z "$iprefs_line" ]]; then
    echo "==> IPrefs relocation: IPrefs not found in Startup-Sequence, skipping"
    return 0
  fi

  local endcli_line
  endcli_line=$(grep -inE '^[[:space:]]*EndCLI' "$tmp_ss" | tail -1 | cut -d: -f1 || true)

  if [[ -z "$endcli_line" ]]; then
    echo "==> IPrefs relocation: EndCLI not found, skipping"
    return 0
  fi

  echo "==> IPrefs relocation: IPrefs at line ${iprefs_line}, EndCLI at line ${endcli_line}"

  awk -v iprefs_ln="$iprefs_line" -v endcli_ln="$endcli_line" '
    NR == iprefs_ln {
      print "; Amiga Imager: IPrefs relocated after LoadWB (avoid RTG requester)"
      next
    }
    NR == endcli_ln {
      print "Run >NIL: IPrefs"
      print ""
    }
    { print }
  ' "$tmp_ss" > "$tmp_ss.new"

  if ! cmp -s "$tmp_ss" "$tmp_ss.new"; then
    mv "$tmp_ss.new" "$tmp_ss"
    make_lf_only "$tmp_ss"
    chmod +x "$tmp_ss"  # Amiga 's' bit: Startup-Sequence is an AmigaDOS script
    fs_copy_file "$tmp_ss" "$sys_root/S/Startup-Sequence"
    echo "==> Patched: IPrefs relocated after LoadWB (Run >NIL: IPrefs before EndCLI)"
  else
    rm -f "$tmp_ss.new"
  fi
}

harden_first_boot_scripts_classic() {
  local sys_root="$1"

  echo "==> Hardening first-boot scripts (Classic Amiga)"

  # 1) OnetimeRunWB launcher:
  #    Run only known scripts (localesettings/RunAll) so package payload
  #    files (.lha, metadata) are never executed as scripts.
  #    Classic has no EMU68INFO — use reboot command instead.
  local tmp_wb="$STAGE/OnetimeRunWB.harden"
  cat > "$tmp_wb" <<'__ONETIMERUNWB_HARDEN__'
; Amiga Imager hardening: execute only known first-boot scripts, silently.
WAIT 1
IF $FIRSTTIMEBOOTWB EQ "TRUE"
   ECHO "DELETE >NIL: SYS:WBStartup/OneTimeRunWB.info ALL FORCE *nDELETE >NIL: SYS:WBStartup/OneTimeRunWB ALL FORCE" to S:OneTimeRun/DeleteOneTimeRunWB
   ; Restore disabled WBStartup entries early so they are not lost if optional
   ; installers fail later in this script.
   COPY >NIL: "SYS:WBStartup/Disabled/#?" TO "SYS:WBStartup/" ALL FORCE CLONE

   IF EXISTS "S:OneTimeRunWB/RunAll"
      Execute "S:OneTimeRunWB/RunAll" >NIL:
      Delete "S:OneTimeRunWB/RunAll" >NIL:
   ENDIF

   IF EXISTS "S:OneTimeRunWB/localesettings"
      Execute "S:OneTimeRunWB/localesettings" >NIL:
      Delete "S:OneTimeRunWB/localesettings" >NIL:
   ENDIF
   DELETE >NIL: "SYS:WBStartup/Disabled" ALL FORCE
   Wait 1
   Sys:c/diskchange DH0:
   Sys:c/reboot
   ENDCLI
ENDIF
__ONETIMERUNWB_HARDEN__
  make_lf_only "$tmp_wb"
  chmod +x "$tmp_wb"  # Amiga 's' bit: required for WBStartup script execution
  fs_copy_file "$tmp_wb" "$sys_root/WBStartup/OnetimeRunWB"

  # 2) iBrowse codec patch helper: skip when iBrowse/spatch payload is not present.
  local tmp_ib="$STAGE/Ibrowse.harden"
  if fs_pull_file "$sys_root/S/OneTimeRun/Ibrowse" "$tmp_ib"; then
    echo "==> Hardening: found OneTimeRun/Ibrowse script from Assets"
    # Check if specific codec files exist (dispatch-aware — works with both engines)
    local has_codecs=0
    if fs_dir "$sys_root/Programs/Ibrowse/codecs/gif.codec" || \
       fs_dir "$sys_root/Programs/Ibrowse/codecs/jpeg.codec" || \
       fs_dir "$sys_root/Programs/Ibrowse/codecs/png.codec"; then
      has_codecs=1
    fi
    echo "==> Hardening: IBrowse codecs present = $has_codecs"
    if [[ "$has_codecs" -eq 1 ]]; then
      cat > "$tmp_ib" <<'__IBROWSE_HARDEN__'
; Amiga Imager hardening: patch iBrowse codecs only when prerequisites exist.
IF NOT EXISTS C:spatch
   SKIP END
ENDIF
IF NOT EXISTS SYS:Programs/Ibrowse/codecs/gif.codec
   SKIP END
ENDIF
IF NOT EXISTS SYS:Programs/Ibrowse/codecs040fpu/gif.codec.spatch
   SKIP END
ENDIF
C:spatch -oSYS:Programs/Ibrowse/codecs/gif.codec -pSYS:Programs/Ibrowse/codecs040fpu/gif.codec.spatch SYS:Programs/Ibrowse/codecs/gif.codec
C:spatch -oSYS:Programs/Ibrowse/codecs/jpeg.codec -pSYS:Programs/Ibrowse/codecs040fpu/jpeg.codec.spatch SYS:Programs/Ibrowse/codecs/jpeg.codec
C:spatch -oSYS:Programs/Ibrowse/codecs/png.codec -pSYS:Programs/Ibrowse/codecs040fpu/png.codec.spatch SYS:Programs/Ibrowse/codecs/png.codec
C:spatch -oSYS:Programs/Ibrowse/codecs/xbm.codec -pSYS:Programs/Ibrowse/codecs040fpu/xbm.codec.spatch SYS:Programs/Ibrowse/codecs/xbm.codec
C:spatch -oSYS:Programs/Ibrowse/codecs/imagedecode.module -pSYS:Programs/Ibrowse/codecs040fpu/imagedecode.module.spatch SYS:Programs/Ibrowse/codecs/imagedecode.module
DELETE SYS:Programs/Ibrowse/codecs040fpu/#? SYS:Programs/Ibrowse/codecs040fpu ALL FORCE QUIET
MAKEDIR SYS:Programs/Ibrowse/cache
LAB END
__IBROWSE_HARDEN__
    else
      echo "==> IBrowse codecs not present; neutralizing OneTimeRun/Ibrowse"
      cat > "$tmp_ib" <<'__IBROWSE_NOOP__'
; Amiga Imager: IBrowse codecs not present, skipping patch.
LAB END
__IBROWSE_NOOP__
    fi
    make_lf_only "$tmp_ib"
    chmod +x "$tmp_ib"  # Amiga 's' bit: required for Execute in T:RunTimeScript
    fs_copy_file "$tmp_ib" "$sys_root/S/OneTimeRun/Ibrowse"
  fi
}


# ===========================================================================
# MAIN PIPELINE
# ===========================================================================
# -----------------------------------------------------------------------------
# Parse flags
# -----------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    --total-gb)        TOTAL_GB="${2:?}"; shift 2;;
    --total-bytes)     TOTAL_BYTES_OVERRIDE="${2:?}"; shift 2;;
    --dh0-mib)          DH0_MIB="${2:?}"; shift 2;;
    --dh1-mib)          DH1_MIB="${2:?}"; shift 2;;
    --amiga-fs)         AMIGA_FS="${2:?}"; shift 2;;
    --pfs3-aio)         PFS3_AIO="${2:?}"; shift 2;;
    --max-transfer)     MAX_TRANSFER="${2:?}"; shift 2;;
    --mask)             MASK="${2:?}"; shift 2;;
    --hst-imager)       HST_IMAGER="${2:?}"; shift 2;;
    --amiga-tools)      AMIGA_TOOLS="${2:?}"; shift 2;;
    --transfer-dir)     TRANSFER_DIR="${2:?}"; shift 2;;
    --transfer-partition) TRANSFER_PARTITION="${2:?}"; shift 2;;
    --partition-content-map) PARTITION_CONTENT_MAP="${2:?}"; shift 2;;
    --adf-dir)          ADF_DIR="${2:?}"; shift 2;;
    --icon-set)         ICON_SET="${2:?}"; shift 2;;
    --packages)         PACKAGES="${2:?}"; shift 2;;
    --locales)          LOCALES="${2:?}"; shift 2;;
    --csv-dir)          CSV_DIR="${2:?}"; shift 2;;
    --aminet-dir)       AMINET_DIR="${2:?}"; shift 2;;
    --packages-dir)     AMINET_DIR="${2:?}"; IBROWSE_DIR="${2:?}"; shift 2;;  # unified staging dir (Aminet + direct + user packages)
    --aminet-rules-csv) AMINET_RULES_CSV="${2:?}"; shift 2;;
    --download-only)    shift 2;;  # removed — no longer used
    --debug)            DEBUG=1; shift 1;;
    --debug-export-dir) DEBUG_EXPORT_DIR="${2:?}"; shift 2;;
    --hst-amiga)        shift 2;;
    --ibrowse-dir)      IBROWSE_DIR="${2:?}"; shift 2;;
    --ibrowse-key)      IBROWSE_KEY="${2:?}"; shift 2;;
    --govd-key)         GOVD_KEY="${2:?}"; shift 2;;
    --build-manifest)   BUILD_MANIFEST="${2:?}"; shift 2;;
    --build-plan)       BUILD_PLAN="${2:?}"; shift 2;;
    --icon-pass2-mode)  ICON_PASS2_MODE="${2:?}"; shift 2;;
    --blank-amiga-partitions) BLANK_AMIGA_PARTITIONS="${2:?}"; shift 2;;
    --include-wbdock|--include-igame|--include-lumiftp|--include-amigaamp|--include-riva|--include-bass|--include-fotaq|--include-lure) shift;; # removed in Stage C; IDs now travel via --packages
    --whdload-kickstarts-dir) WHDLOAD_KICKSTARTS_DIR="${2:?}"; shift 2;;
    --imp3-reg)             IMP3_REG="${2:?}"; shift 2;;
    --imp3-reg-file)        IMP3_REG_FILE="${2:?}"; shift 2;;
    --boing-bag-1)          BOING_BAG_1="${2:?}"; shift 2;;
    --boing-bag-2)          BOING_BAG_2="${2:?}"; shift 2;;
    --backdrop-enabled) BACKDROP_ENABLED="${2:?}"; shift 2;;
    --partition)        partition_layout_add_spec "${2:?}"; shift 2;;
    # Screen mode / RTG (Classic supports optional RTG cards)
    --screen-width)     SCREEN_WIDTH="${2:?}"; shift 2;;
    --screen-height)    SCREEN_HEIGHT="${2:?}"; shift 2;;
    --screen-fps)       SCREEN_FPS="${2:?}"; shift 2;;
    --screen-interlace) SCREEN_INTERLACE="${2:?}"; shift 2;;
    --screen-mode-id)   SCREEN_MODE_ID="${2:?}"; shift 2;;
    --wb-depth)         WB_DEPTH="${2:?}"; shift 2;;
    --wb-depth-hex)     WB_DEPTH_HEX="${2:?}"; shift 2;;
    --p96-icomp-archive) P96_ICOMP_ARCHIVE="${2:?}"; shift 2;;
    # Classic hardware
    --classic-machine)  CLASSIC_MACHINE="${2:?}"; shift 2;;
    --accelerator)      ACCELERATOR="${2:?}"; shift 2;;
    --rtg-card)         RTG_CARD="${2:?}"; shift 2;;
    --rtg-card-driver)  RTG_CARD_DRIVER="${2:?}"; shift 2;;
    # Network (Classic supports Roadshow but not genet/wifipi)
    --network-card)         NETWORK_CARD="${2:?}"; shift 2;;
    --network-card-device)  NETWORK_CARD_DEVICE="${2:?}"; shift 2;;
    --a314-clockport-address) A314_CONFIG_CLOCKPORT_ADDRESS="${2:?}"; shift 2;;
    --a314-interrupt)         A314_CONFIG_INTERRUPT="${2:?}"; shift 2;;
    --a314-pidrive-automount) A314_PIDRIVE_AUTOMOUNT="${2:?}"; shift 2;;
    --network-stack)    NETWORK_STACK="${2:?}"; shift 2;;
    --roadshow-variant) ROADSHOW_VARIANT="${2:?}"; shift 2;;
    --roadshow-archive) ROADSHOW_ARCHIVE="${2:?}"; shift 2;;
    # Silently ignore PiStorm-only flags that the Swift app may pass
    --fat-mib|--emu68-boot-url|--emu68-boot-mode|--has-floppy|--mount-emu68boot|--emu68boot-device)
      shift 2;;
    --unicam-enable|--unicam-autostart|--unicam-scaling|--unicam-brightness|--unicam-contrast)
      shift 2;;
    --unicam-sizew|--unicam-sizeh|--unicam-offsetx|--unicam-offsety)
      shift 2;;
    --wifi-ssid|--wifi-password)
      shift 2;;
    *) die "Unknown argument: $1";;
  esac
done

load_build_plan "${BUILD_PLAN:-}"

# Derive INCLUDE_* flags from PACKAGES list (all package IDs travel via --packages since Stage C)
package_is_selected "ibrowse"  && INCLUDE_IBROWSE=1  || true
package_is_selected "wbdock"   && INCLUDE_WBDOCK=1   || true
package_is_selected "bass"     && INCLUDE_BASS=1     || true
package_is_selected "fotaq"    && INCLUDE_FOTAQ=1    || true
package_is_selected "lure"     && INCLUDE_LURE=1     || true
package_is_selected "govd"     && INCLUDE_GOVD=1     || true

# Stage D: log resolved build manifest (written by Swift, additive/informational only)
if [[ -n "${BUILD_MANIFEST:-}" && -f "${BUILD_MANIFEST:-}" ]]; then
  echo "==> [manifest] Resolved build plan:"
  cat "$BUILD_MANIFEST"
fi

# -----------------------------------------------------------------------------
# Normalize filesystem token for hst-imager
# -----------------------------------------------------------------------------
HST_FS=""
case "$AMIGA_FS" in
  Ffs|FFS)
    AMIGA_FS="Ffs"
    HST_FS="DOS3"
    ;;
  FfsDC)
    AMIGA_FS="FfsDC"
    HST_FS="DOS7"
    ;;
  Pfs3|PFS3)
    AMIGA_FS="Pfs3"
    HST_FS="Pfs3"
    ;;
  *)
    die "Unsupported filesystem: $AMIGA_FS (use Ffs | FfsDC | Pfs3)"
    ;;
esac

# -----------------------------------------------------------------------------
# Validate inputs
# -----------------------------------------------------------------------------
if [[ "$BLANK_AMIGA_PARTITIONS" -ne 1 ]]; then
  [[ -f "$KICK_ROM" ]] || die "Kickstart ROM missing: $KICK_ROM"
fi
[[ -d "$ASSETS_DIR" ]] || die "Assets dir missing: $ASSETS_DIR"
[[ -x "$HST_IMAGER" ]] || die "hst-imager not found/executable: $HST_IMAGER"

if [[ -n "$ISO_PATH" ]]; then
  [[ -f "$ISO_PATH" ]] || die "ISO missing: $ISO_PATH"
fi

if [[ -n "$ADF_DIR" ]]; then
  [[ -d "$ADF_DIR" ]] || die "ADF dir missing: $ADF_DIR"
fi

if [[ -n "$AMINET_RULES_CSV" ]]; then
  [[ -f "$AMINET_RULES_CSV" ]] || die "Aminet rules CSV missing: $AMINET_RULES_CSV"
fi

if [[ -z "$ISO_PATH" && -z "$ADF_DIR" ]]; then
  if [[ "$BLANK_AMIGA_PARTITIONS" -eq 1 && "$HST_FS" == "Pfs3" ]]; then
    :
  elif [[ "$BLANK_AMIGA_PARTITIONS" -eq 1 ]]; then
    die "Blank FFS images need --adf-dir or an ISO path so FastFileSystem can be extracted. Use PFS3 to build without media."
  else
    die "No input media available. Provide --adf-dir (preferred) or an ISO path as 3rd positional argument."
  fi
fi

case "$ICON_SET" in
  standard|glowicons) ;;
  *) die "Invalid --icon-set. Use: standard | glowicons" ;;
esac

# --- N-partition support: populate partition arrays from --partition args or legacy fallback ---
partition_layout_prepare "DH" 1

[[ "$MAX_TRANSFER" =~ ^0[xX][0-9a-fA-F]+$ ]] || die "Invalid --max-transfer '$MAX_TRANSFER'. Use a hexadecimal value such as 0x1fe00."
[[ "$MASK" =~ ^0[xX][0-9a-fA-F]+$ ]] || die "Invalid --mask '$MASK'. Use a hexadecimal value such as 0x7ffffffe."

if partition_layout_uses_fs "Pfs3"; then
  [[ -f "$PFS3_AIO" ]] || die "PFS3 selected but pfs3aio not found: $PFS3_AIO"
fi

have_cmd curl  || die "curl is required."
have_cmd unzip || die "unzip is required."

if [[ -n "${TOTAL_BYTES_OVERRIDE:-}" && "$TOTAL_BYTES_OVERRIDE" -gt 0 ]] 2>/dev/null; then
  DISK_SIZE_BYTES="$TOTAL_BYTES_OVERRIDE"
else
  DISK_SIZE_BYTES=$(( TOTAL_GB * 1000 * 1000 * 1000 ))
fi

echo "==> Layout (Classic Amiga RDB):"
echo "    Total: ${TOTAL_GB} GB (${DISK_SIZE_BYTES} bytes)"
echo "    Partitions: ${NUM_PARTS}"
for _i in $(seq 0 $((NUM_PARTS - 1))); do
  echo "      ${PART_NAMES[$_i]}: label=${PART_LABELS[$_i]} size=${PART_SIZES[$_i]}MiB fs=${PART_FSTYPES[$_i]} block=${PART_BLOCKSIZES[$_i]} boot=${PART_BOOTABLE[$_i]} pri=${PART_BOOTPRI[$_i]}"
done
echo "    FS:    ${AMIGA_FS} (dostype=${HST_FS})"
echo "    ADF:   ${ADF_DIR:-<auto-detect from ISO/ADF>}"
echo "    ISO:   ${ISO_PATH:-<none>}"
echo "    Icons: ${ICON_SET}"
echo "    Locales: ${LOCALES:-<all>}"
echo "    Pkgs:  ${PACKAGES:-<none>}"
echo "    Blank partitions: ${BLANK_AMIGA_PARTITIONS}"
echo "    Machine: ${CLASSIC_MACHINE:-<not set>}"
echo "    Accelerator: ${ACCELERATOR:-<stock>}"
echo "    RTG Card: ${RTG_CARD:-<none>}"
echo "    Network Card: ${NETWORK_CARD:-<none>} (${NETWORK_CARD_DEVICE:-<none>})"
echo "    RDB transfer: MaxTransfer=${MAX_TRANSFER} Mask=${MASK}"
echo "    Network Stack: ${NETWORK_STACK}"
echo "    Aminet-dir: ${AMINET_DIR:-<none>}"
echo "    Debug: ${DEBUG} (export-dir=${DEBUG_EXPORT_DIR:-<img directory>})"
echo "    Tools: amiga-tools=${AMIGA_TOOLS:-<not set>}"

# -----------------------------------------------------------------------------
# Stage + ISO mount
# -----------------------------------------------------------------------------
STAGE="$(mktemp -d "/tmp/classic-stage.XXXXXX")"
MNT="$STAGE/mnt"
mkdir -p "$MNT"
ISO_ROOT="$STAGE/no-iso"
ISO_MOUNTED=0

cleanup() {
  export_debug_hdf || true
  if [[ "${ISO_MOUNTED:-0}" -eq 1 ]]; then
    hdiutil detach "$MNT" -force >/dev/null 2>&1 || true
  fi
  rm -rf "$STAGE" >/dev/null 2>&1 || true
}
trap cleanup EXIT

if [[ -n "$ISO_PATH" ]]; then
  echo "==> Mounting ISO via hdiutil (read-only)"
  _already_mounted="$("${AMIGA_TOOLS}" hdiutil-find-mount "$ISO_PATH" 2>/dev/null || true)"
  if [[ -n "$_already_mounted" ]]; then
    echo "==> ISO already mounted at $_already_mounted (reusing)"
    MNT="$_already_mounted"
  else
    hdiutil attach "$ISO_PATH" -readonly -nobrowse -mountpoint "$MNT" >/dev/null
    ISO_MOUNTED=1
  fi
  ISO_ROOT="$MNT"
else
  echo "==> No ISO provided; using ADF-only mode"
fi

# -----------------------------------------------------------------------------
# Find FastFileSystem (needed for DOS3/DOS7)
# -----------------------------------------------------------------------------
FFS_PATH=""

find_ffs_in_adf_dir() {
  local adf_dir="$1"
  [[ -d "$adf_dir" ]] || return 1

  local tmp_ffs_root="$STAGE/ffs-scan"
  rm -rf "$tmp_ffs_root" >/dev/null 2>&1 || true
  mkdir -p "$tmp_ffs_root"

  local -a adf_candidates=()
  local _f

  # Top-level priority: Workbench/Install ADFs first
  for _f in "$adf_dir"/Workbench*.adf "$adf_dir"/Workbench*.ADF "$adf_dir"/Install*.adf "$adf_dir"/Install*.ADF; do
    [[ -f "$_f" ]] && adf_candidates+=("$_f")
  done
  for _f in "$adf_dir"/*.adf "$adf_dir"/*.ADF; do
    [[ -f "$_f" ]] && adf_candidates+=("$_f")
  done
  # Deep scan: find ADFs in subdirectories too (handles user-provided trees
  # like /Volumes/AMIGA/OS3.2/Disks/Workbench.adf)
  while IFS= read -r _f; do
    [[ -n "$_f" && -f "$_f" ]] && adf_candidates+=("$_f")
  done < <(find "$adf_dir" -type f \( -iname 'Workbench*.adf' -o -iname 'Install*.adf' \) 2>/dev/null)

  # De-duplicate while preserving order.
  local -a deduped=()
  local seen="|"
  local item
  [[ ${#adf_candidates[@]} -eq 0 ]] && return 1
  for item in "${adf_candidates[@]}"; do
    if [[ "$seen" == *"|$item|"* ]]; then
      continue
    fi
    deduped+=("$item")
    seen="${seen}${item}|"
  done

  local adf
  for adf in "${deduped[@]}"; do
    local adf_out="$tmp_ffs_root/$(basename "$adf").extract"
    rm -rf "$adf_out" >/dev/null 2>&1 || true
    mkdir -p "$adf_out"

    # Native extractor first (keeps the native path free of hst-imager calls);
    # hst-imager remains the fallback for ADF DOS types it cannot read.
    if ! { [[ -n "${AMIGA_TOOLS:-}" && -x "${AMIGA_TOOLS:-}" ]] \
           && "$AMIGA_TOOLS" adf extract "$adf" "$adf_out" >/dev/null 2>&1; } \
      && ! "$HST_IMAGER" fs extract "$adf" "$adf_out" --recursive --force >/dev/null 2>&1 \
      && ! "$HST_IMAGER" fs extract "$adf" "$adf_out" --recursive >/dev/null 2>&1 \
      && ! "$HST_IMAGER" fs copy "$adf" "$adf_out" --recursive --force >/dev/null 2>&1 \
      && ! "$HST_IMAGER" fs copy "$adf" "$adf_out" --recursive >/dev/null 2>&1; then
      continue
    fi

    local ffs_bin=""
    ffs_bin="$(find "$adf_out" -type f -iname 'FastFileSystem' 2>/dev/null | head -n1)"
    if [[ -n "$ffs_bin" && -f "$ffs_bin" && -s "$ffs_bin" ]]; then
      echo "$ffs_bin"
      return 0
    fi
  done
  return 1
}

if [[ "$HST_FS" != "Pfs3" ]]; then
  echo "==> Locating FastFileSystem handler..."

  # Try ISO L/ first
  if [[ -n "$ISO_PATH" ]]; then
    _iso_ffs="$(find "$ISO_ROOT" -path '*/L/FastFileSystem' -type f 2>/dev/null | head -n1 || true)"
    if [[ -n "$_iso_ffs" && -f "$_iso_ffs" ]]; then
      FFS_PATH="$_iso_ffs"
      echo "==> Found FastFileSystem from ISO: $FFS_PATH"
    fi
  fi

  # Try ADF dir
  if [[ -z "$FFS_PATH" && -n "$ADF_DIR" ]]; then
    _ffs_from_adf="$(find_ffs_in_adf_dir "$ADF_DIR" || true)"
    if [[ -n "$_ffs_from_adf" && -f "$_ffs_from_adf" ]]; then
      FFS_PATH="$_ffs_from_adf"
      echo "==> Found FastFileSystem from ADF: $FFS_PATH"
    fi
  fi

  [[ -n "$FFS_PATH" ]] || die "FastFileSystem not found in ADF directory or ISO L/. Cannot format ${HST_FS}."
fi

# -----------------------------------------------------------------------------
# 1-2) Create blank HDF image and initialize RDB
# -----------------------------------------------------------------------------
echo "==> PROGRESS 1/10 Creating blank image"
mkdir -p "$(dirname "$IMG")"
if [[ -e "$IMG" ]]; then
  echo "==> Removing existing image: $IMG"
  rm -f "$IMG"
fi

if [[ "${DISK_ENGINE:-hst}" == "amigadiskkit" ]]; then
  # Native engine: blank file + RDB creation happen together in step 3 (rdb-build).
  # Compute USABLE_BYTES from disk size now so partition_layout_resolve_bytes can run.
  echo "==> 1) [amigadiskkit] HDF image (${TOTAL_GB} GB) — creation deferred to step 3"
  echo "==> PROGRESS 2/10 Initializing RDB (deferred)"
  echo "==> 2) [amigadiskkit] RDB init deferred — native engine creates image + RDB atomically"
  _native_pure_rdb_usable_bytes "$DISK_SIZE_BYTES"
  RDB="$IMG/rdb"
else
  # hst-imager path: blank + rdb init + register filesystem handlers
  echo "==> 1) Create blank HDF image (${TOTAL_GB} GB)"
  run "$HST_IMAGER" blank "$IMG" "$DISK_SIZE_BYTES"
  echo "==> PROGRESS 2/10 Initializing RDB"
  echo "==> 2) Initialize RDB on HDF"
  run "$HST_IMAGER" rdb init "$IMG" --size "$DISK_SIZE_BYTES"
  if [[ "$HST_FS" == "Pfs3" ]]; then
    run "$HST_IMAGER" rdb fs add "$IMG" "$PFS3_AIO" PDS3
  else
    run "$HST_IMAGER" rdb fs add "$IMG" "$FFS_PATH" "$HST_FS"
    if partition_layout_uses_fs "Pfs3"; then
      run "$HST_IMAGER" rdb fs add "$IMG" "$PFS3_AIO" PDS3
    fi
  fi
  RDB="$IMG/rdb"
  if ! "$HST_IMAGER" rdb info "$RDB" >/dev/null 2>&1; then
    die "RDB init failed: RDB not accessible at $RDB"
  fi
fi

# -----------------------------------------------------------------------------
# 3) Partition layout (N partitions)
# -----------------------------------------------------------------------------
echo "==> PROGRESS 3/10 Creating partitions"
echo "==> 3) Partition layout (${NUM_PARTS} partitions)"

TAIL_SAFETY_BYTES=$(( 32 * 1024 * 1024 ))
HEAD_SAFETY_BYTES=$(( 4 * 1024 * 1024 ))

if [[ "${DISK_ENGINE:-hst}" != "amigadiskkit" ]]; then
  # hst-imager path: read RDB info for geometry + usable space
  RDB_INFO="$("$HST_IMAGER" rdb info "$RDB" --verbose 2>/dev/null || true)"
  echo "$RDB_INFO" || true
  RDB_BYTES="$(echo "$RDB_INFO" | sed -nE 's/.*\(([0-9]+) bytes\).*/\1/p' | head -n1)"
  [[ -n "$RDB_BYTES" ]] || die "Failed to determine RDB size in bytes from: hst-imager rdb info $RDB"
  UNALLOC_BYTES="$(echo "$RDB_INFO" | sed -nE 's/.*[Uu]nallocated.*\(([0-9]+) bytes\).*/\1/p' | head -n1)"
  if [[ -n "${UNALLOC_BYTES:-}" && "${UNALLOC_BYTES:-0}" -gt 0 ]]; then
    USABLE_BYTES=$(( UNALLOC_BYTES - TAIL_SAFETY_BYTES ))
  else
    USABLE_BYTES=$(( RDB_BYTES - HEAD_SAFETY_BYTES - TAIL_SAFETY_BYTES ))
  fi
  [[ "$USABLE_BYTES" -gt 0 ]] || die "Computed usable RDB space invalid"
fi

partition_layout_resolve_bytes "$USABLE_BYTES"
partition_layout_log_resolved_bytes "$RDB_BYTES" "$USABLE_BYTES"

# When PFS3 is the primary filesystem, force ALL partitions to PFS3.
if [[ "$HST_FS" == "Pfs3" ]]; then
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    PART_FSTYPES[$_i]="Pfs3"
  done
  echo "==> PFS3 selected: forced all ${NUM_PARTS} partitions to PFS3"
fi

# Auto-upgrade large FFS data partitions to FfsDC (DOS7) — see common.sh helper.
upgrade_large_ffs_data_partitions

if [[ "${DISK_ENGINE:-hst}" == "amigadiskkit" ]]; then
  # -------------------------------------------------------------------------
  # Native engine: create blank image + RDB + partitions in one rdb-build call
  # -------------------------------------------------------------------------
  echo "==> [amigadiskkit] Building RDB partition layout"
  _native_build_part_args "$NUM_PARTS"
  run "$AMIGA_TOOLS" disk rdb-build "$IMG" "$DISK_SIZE_BYTES" "${NATIVE_PART_ARGS[@]}"

  # Register filesystem handlers (image exists only after rdb-build).
  # Mirrors the hst-imager branch above; rdb-fs-add is idempotent.
  if [[ "$HST_FS" == "Pfs3" ]]; then
    run "$AMIGA_TOOLS" disk rdb-fs-add "$IMG" "$PFS3_AIO" PDS3
  else
    if [[ "${NEEDS_DOS7_FS_REGISTRATION:-0}" == "1" ]]; then ensure_rdb_has_dos7_fs; fi
    if partition_layout_uses_fs "Pfs3"; then
      run "$AMIGA_TOOLS" disk rdb-fs-add "$IMG" "$PFS3_AIO" PDS3
    fi
  fi

  # Resolve partition numbers from the native-written RDB (native rdb-info;
  # numbers equal PART-chain order, which rdb-build/reinit wrote in our order)
  native_resolve_part_pns

  # Format each partition (native FFS formatter — no Bug 1/2)
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    run "$AMIGA_TOOLS" disk rdb-format "$IMG" "${PART_NAMES[$_i]}" "${PART_LABELS[$_i]}"
    echo "==> Partition ${PART_NAMES[$_i]} (${PART_LABELS[$_i]}) post-format: OK (native engine)"
  done
else
  # -------------------------------------------------------------------------
  # hst-imager path: delete + add + format + verify
  # -------------------------------------------------------------------------
  echo "==> Cleaning existing RDB partitions (robust)"
  for pn in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
    "$HST_IMAGER" rdb part delete "$RDB" "$pn" >/dev/null 2>&1 || true
  done

  # Create N partitions
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    _dostype="$(part_fs_to_dostype "${PART_FSTYPES[$_i]}" "${PART_BOOTABLE[$_i]}")"
    _boot_flag="false"
    [[ "${PART_BOOTABLE[$_i]}" == "1" ]] && _boot_flag="true"
    run "$HST_IMAGER" rdb part add "$RDB" "${PART_NAMES[$_i]}" "$_dostype" "${PART_BYTES[$_i]}" \
      --reserved 2 --block-size "${PART_BLOCKSIZES[$_i]}" --max-transfer "$MAX_TRANSFER" --mask "$MASK" \
      --bootable "$_boot_flag" --boot-priority "${PART_BOOTPRI[$_i]}"
  done

  # Resolve partition numbers
  RDB_INFO2="$("$HST_IMAGER" rdb info "$RDB" --verbose 2>/dev/null || true)"
  PART_PNS=()
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    _pn="$(rdb_partnum_by_name "$RDB_INFO2" "${PART_NAMES[$_i]}")"
    [[ -n "$_pn" ]] || die "Failed to resolve partition number for ${PART_NAMES[$_i]}"
    PART_PNS+=("$_pn")
  done

  # Format each partition with its label
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    run "$HST_IMAGER" rdb part format "$RDB" "${PART_PNS[$_i]}" "${PART_LABELS[$_i]}"
  done

  # Verify every partition is mountable after format (same guard as build-pistorm.sh).
  # See build-pistorm.sh for explanation of the two hst-imager bugs handled here.
  # For Classic (pure RDB, no MBR) the RDB slice is the image itself, so _rdb_disk_start=0.
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    _pn="${PART_PNS[$_i]}"
    _vpath="$IMG/rdb/$_pn"
    if fs_dir "$_vpath"; then
      echo "==> Partition ${PART_NAMES[$_i]} (${PART_LABELS[$_i]}) post-format: OK"
      continue
    fi
    _slice_start=$("$HST_IMAGER" info "$IMG" 2>/dev/null \
      | awk -F'|' -v pn="$_pn" '
          /RDB/ {
            n=$4; gsub(/[[:space:]]+/, "", n)
            if (n == pn) {
              s=$6; gsub(/[[:space:]]+/, "", s)
              if (s ~ /^[0-9]+$/) { print s; exit }
            }
          }')
    if [[ -z "$_slice_start" ]]; then
      echo "==> ERROR: Partition ${PART_NAMES[$_i]} (pn $_pn, ${PART_LABELS[$_i]}) failed post-format check." >&2
      echo "==>   hst-imager fs dir failed and start offset not found in info output." >&2
      echo "==>   FS type: ${PART_FSTYPES[$_i]} → dostype $(part_fs_to_dostype "${PART_FSTYPES[$_i]}" "${PART_BOOTABLE[$_i]}")" >&2
      die "Post-format check failed for partition ${PART_NAMES[$_i]}"
    fi
    _dd_block=$(( _slice_start / 512 ))
    _dd_off=$(( _slice_start % 512 ))
    _raw_magic=$(dd if="$IMG" bs=512 skip="$_dd_block" count=1 2>/dev/null \
      | dd bs=1 skip="$_dd_off" count=4 2>/dev/null \
      | od -A n -t x1 \
      | tr -d ' \n')
    _dostype=$(part_fs_to_dostype "${PART_FSTYPES[$_i]}" "${PART_BOOTABLE[$_i]}")
    case "$_dostype" in
      DOS3) _expected_hex="444f5303" ;;
      DOS7) _expected_hex="444f5307" ;;
      PDS3) _expected_hex="50445303" ;;
      *)    _expected_hex="" ;;
    esac
    if [[ -n "$_expected_hex" && "$_raw_magic" == "$_expected_hex" ]]; then
      echo "==> Partition ${PART_NAMES[$_i]} (${PART_LABELS[$_i]}) post-format: OK (raw boot block check)"
      echo "==>   (hst-imager fs dir skipped: uint32 overflow for partition at byte $_slice_start)"
      continue
    fi
    echo "==> ERROR: Partition ${PART_NAMES[$_i]} (pn $_pn, ${PART_LABELS[$_i]}) failed post-format check." >&2
    echo "==>   FS type: ${PART_FSTYPES[$_i]} → dostype $_dostype (expected: ${_expected_hex:-unknown})" >&2
    echo "==>   Boot block at byte $_slice_start: 0x${_raw_magic}" >&2
    echo "==>   For FFS partitions > 2 GiB, upgrade_large_ffs_data_partitions should prevent this." >&2
    die "Post-format check failed for partition ${PART_NAMES[$_i]}"
  done

  # Bug 1 guard: explicitly verify boot partition FFS boot block checksum.
  # See build-pistorm.sh for full explanation. For Classic (pure RDB, no MBR),
  # the partition start byte from hst-imager info "$IMG" is the absolute offset.
  _bug1_pn="${PART_PNS[0]}"
  _bug1_slice_start=$("$HST_IMAGER" info "$IMG" 2>/dev/null \
    | awk -F'|' -v pn="$_bug1_pn" '/RDB/ {
        n=$4; gsub(/[[:space:]]+/, "", n)
        if (n == pn) {
          s=$6; gsub(/[[:space:]]+/, "", s)
          if (s ~ /^[0-9]+$/) { print s; exit }
        }
      }')
  if [[ -n "$_bug1_slice_start" ]]; then
    _bug1_blk=$(( _bug1_slice_start / 512 ))
    _bug1_cksum=$(dd if="$IMG" bs=512 skip="$_bug1_blk" count=1 2>/dev/null \
      | dd bs=1 skip=4 count=4 2>/dev/null \
      | od -A n -t x1 | tr -d ' \n')
    _bug1_rootptr=$(dd if="$IMG" bs=512 skip="$_bug1_blk" count=1 2>/dev/null \
      | dd bs=1 skip=8 count=4 2>/dev/null \
      | od -A n -t x1 | tr -d ' \n')
    if [[ "$_bug1_cksum" == "00000000" && "$_bug1_rootptr" == "00000000" ]]; then
      echo "==> ERROR: Boot partition ${PART_NAMES[0]} (${PART_LABELS[0]}) has a corrupt boot block." >&2
      echo "==>   FFS checksum and root-block pointer are both zero — hst-imager Bug 1." >&2
      echo "==>   DOS3 partitions that exceed 2 GiB (including a default 2 GiB DH0 after" >&2
      echo "==>   cylinder-alignment) are silently corrupted by hst-imager rdb part format." >&2
      echo "==>   AmigaOS shows 'Not a DOS disk in device ${PART_NAMES[0]}' on first boot." >&2
      echo "==>   Fix: Settings → Debug → Disk Engine → Native (recommended)." >&2
      echo "==>   Alt: reduce ${PART_NAMES[0]} size to 1.8 GiB or below in the UI." >&2
      die "Boot block corruption (hst-imager Bug 1) in ${PART_NAMES[0]}"
    fi
  fi
fi

# Primary system partition path (pure RDB — no MBR layer)
PARTITION_ROOT_BASE="$IMG/rdb"
SYS_ROOT="$PARTITION_ROOT_BASE/${PART_PNS[0]}"

# Register partition name map for amigadiskkit engine dispatch (see common.sh DISK_ENGINE)
DISK_ENGINE_IMG="$IMG"
DISK_ENGINE_ROOT_BASE="$PARTITION_ROOT_BASE"
DISK_ENGINE_PART_NAMES=()
for _disk_i in "${!PART_NAMES[@]}"; do DISK_ENGINE_PART_NAMES[${PART_PNS[$_disk_i]}]="${PART_NAMES[$_disk_i]}"; done

# Resolve transfer target partition path
TRANSFER_ROOT="$SYS_ROOT"
if [[ -n "$TRANSFER_PARTITION" ]]; then
  _transfer_idx=""
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    if [[ "${PART_NAMES[$_i]}" == "$TRANSFER_PARTITION" ]]; then
      _transfer_idx="$_i"
      break
    fi
  done
  if [[ -n "$_transfer_idx" ]]; then
    TRANSFER_ROOT="$PARTITION_ROOT_BASE/${PART_PNS[$_transfer_idx]}"
  else
    echo "==> WARN: transfer partition '$TRANSFER_PARTITION' not found; using ${PART_NAMES[0]}" >&2
  fi
fi

apply_partition_content_map() {
  [[ -n "$PARTITION_CONTENT_MAP" ]] || return 0
  [[ -f "$PARTITION_CONTENT_MAP" ]] || die "Partition content map missing: $PARTITION_CONTENT_MAP"

  echo "==> Applying partition content map"

  local _map_name _map_path _target_root _idx
  while IFS=$'\t' read -r _map_name _map_path || [[ -n "${_map_name}${_map_path}" ]]; do
    [[ -n "$_map_name" ]] || continue
    [[ "${_map_name#\#}" != "$_map_name" ]] && continue
    [[ -n "$_map_path" ]] || continue
    [[ -d "$_map_path" ]] || die "Partition content directory missing for $_map_name: $_map_path"

    _target_root=""
    for _idx in $(seq 0 $((NUM_PARTS - 1))); do
      if [[ "${PART_NAMES[$_idx]}" == "$_map_name" ]]; then
        _target_root="$PARTITION_ROOT_BASE/${PART_PNS[$_idx]}"
        break
      fi
    done

    [[ -n "$_target_root" ]] || die "Partition content target not found in image: $_map_name"
    echo "==> Copying $(basename "$_map_path") -> $_map_name"
    fs_copy "$_map_path/" "$_target_root/" --recursive --makedir
  done < "$PARTITION_CONTENT_MAP"
}

if [[ "$BLANK_AMIGA_PARTITIONS" -eq 1 ]]; then
  apply_partition_content_map
  echo "==> PROGRESS 4/10 Skipping AmigaOS population (blank partitions mode)"
  echo "==> Blank partitions mode: formatted partitions created and assigned content copied; skipping AmigaOS setup"
  echo "==> PROGRESS 10/10 Complete"
  echo "==> Build complete: $IMG"
  exit 0
fi

# -----------------------------------------------------------------------------
# 4) Populate SYS: from ADFs (Windows-style merge)
# -----------------------------------------------------------------------------
echo "==> PROGRESS 4/10 Extracting AmigaOS from ADFs"
echo "==> 4) Populate SYS: from ADFs (Windows-style merge)"

detect_locale_code_from_adf_name() {
  local adf_name="$1"
  local lower base tail
  lower="$(printf "%s" "$adf_name" | tr '[:upper:]' '[:lower:]')"
  base="${lower%.adf}"

  [[ "$base" == locale* ]] || return 1

  tail="${base#locale}"
  while [[ "$tail" == -* || "$tail" == _* ]]; do
    tail="${tail#?}"
  done

  case "$tail" in
    en*|english*) echo "en"; return 0 ;;
    de*|german*|deutsch*) echo "de"; return 0 ;;
    fr*|french*) echo "fr"; return 0 ;;
    it*|italian*) echo "it"; return 0 ;;
    nl*|dutch*) echo "nl"; return 0 ;;
    uk*|british*) echo "uk"; return 0 ;;
    dk*|danish*) echo "dk"; return 0 ;;
    es*|spanish*) echo "es"; return 0 ;;
    se*|swedish*) echo "se"; return 0 ;;
    gr*|greek*) echo "gr"; return 0 ;;
    no*|norwegian*) echo "no"; return 0 ;;
    pl*|polish*) echo "pl"; return 0 ;;
    pt*|portuguese*) echo "pt"; return 0 ;;
    ru*|russian*) echo "ru"; return 0 ;;
    tr*|turkish*) echo "tr"; return 0 ;;
    cz*|czech*) echo "cz"; return 0 ;;
    rs*|serbian*) echo "rs"; return 0 ;;
  esac

  # Handle versioned names like "Locale3.2.3-DE"
  for _code in en de fr it nl uk dk es se gr no pl pt ru tr cz rs; do
    if echo "$tail" | grep -Eq "(^|[^a-z])${_code}([^a-z]|$)"; then
      echo "$_code"
      return 0
    fi
  done

  return 1
}

is_base_locale_adf_name() {
  local adf_name="$1"
  local lower base tail
  lower="$(printf "%s" "$adf_name" | tr '[:upper:]' '[:lower:]')"
  base="${lower%.adf}"

  [[ "$base" == locale* ]] || return 1

  tail="${base#locale}"
  while [[ "$tail" == -* || "$tail" == _* ]]; do
    tail="${tail#?}"
  done

  [[ -z "$tail" ]] && return 0
  echo "$tail" | grep -Eq '^[0-9.]+$' && return 0
  return 1
}

is_glowicons_adf_name() {
  local adf_name="$1"
  local lower
  lower="$(printf "%s" "$adf_name" | tr '[:upper:]' '[:lower:]')"
  [[ "$lower" == *glow*icon* ]]
}

is_4coloricons_adf_name() {
  local adf_name="$1"
  local lower
  lower="$(printf "%s" "$adf_name" | tr '[:upper:]' '[:lower:]')"
  echo "$lower" | grep -Eq '4[ _-]*(color|colour)[ _-]*icons?'
}

SYS_HOST="$STAGE/sys-merge"
rm -rf "$SYS_HOST" >/dev/null 2>&1 || true
mkdir -p "$SYS_HOST"

declare -a ADFS BASE_ADFS PATCH_ADFS ICON4_ADFS ICONGLOW_ADFS

IS_39_ISO=0
ADF_SRC=""
WORKBENCH_ADF=""
LOCALES="$(printf "%s" "$LOCALES" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"

# ADF_DIR (explicit --adf-dir) always takes priority over ISO content detection.
if [[ -n "$ADF_DIR" ]]; then
  ADF_SRC="$ADF_DIR"
elif [[ -d "$ISO_ROOT/OS-Version3.9" ]]; then
  IS_39_ISO=1
  echo "==> Detected AmigaOS 3.9 CD ISO structure"
elif [[ -d "$ISO_ROOT/ADF" ]]; then
  ADF_SRC="$ISO_ROOT/ADF"
else
  die "No ADF source found. Provide --adf-dir or an ISO with ADF/ or OS-Version3.9/ folder."
fi

detect_workbench_version() {
  local adf_basename
  adf_basename="$(basename "${WORKBENCH_ADF:-}" 2>/dev/null || true)"
  if [[ -z "$adf_basename" ]]; then
    DETECTED_WB_VERSION="3.2"
    echo "==> Workbench version detected: $DETECTED_WB_VERSION (default — no Workbench ADF found)"
    return 0
  fi
  local ver_str=""
  ver_str="$(echo "$adf_basename" | sed -E 's/\.[aA][dD][fF]$//' | sed -E 's/^[Ww]orkbench//')"
  case "$ver_str" in
    3.2.3)   DETECTED_WB_VERSION="3.2.3" ;;
    3.2.2.1) DETECTED_WB_VERSION="3.2.2.1" ;;
    3.2.2)   DETECTED_WB_VERSION="3.2.2" ;;
    3.2.1)   DETECTED_WB_VERSION="3.2.1" ;;
    3.2)     DETECTED_WB_VERSION="3.2" ;;
    3.1.4)   DETECTED_WB_VERSION="3.1.4" ;;
    3.1)     DETECTED_WB_VERSION="3.1" ;;
    3.9*)    DETECTED_WB_VERSION="3.9" ;;
    *)       DETECTED_WB_VERSION="3.2" ;;
  esac
  echo "==> Workbench version detected: $DETECTED_WB_VERSION (from $adf_basename)"
}

if [[ "$IS_39_ISO" -eq 1 ]]; then
  # --- OS 3.9 CD: direct filesystem extraction (no ADFs) ---
  echo "==> Extracting OS 3.9 base files from CD ISO"
  WB35_DIR="$ISO_ROOT/OS-Version3.9/Workbench3.5"
  WB39_DIR="$ISO_ROOT/OS-Version3.9/Workbench3.9"
  [[ -d "$WB35_DIR" ]] || die "OS 3.9 CD missing expected directory: OS-Version3.9/Workbench3.5"

  echo "==> Copying Workbench 3.5 base files to host staging"
  cp -R "$WB35_DIR"/* "$SYS_HOST/"

  if [[ -d "$WB39_DIR" ]]; then
    echo "==> Overlaying Workbench 3.9 updated files"
    cp -R "$WB39_DIR"/* "$SYS_HOST/"
  fi

  if [[ -n "$BOING_BAG_1" && -f "$BOING_BAG_1" ]]; then
    echo "==> Extracting Boing Bag 1: $(basename "$BOING_BAG_1")"
    extract_lha_to_host "$BOING_BAG_1" "$SYS_HOST"
  fi
  if [[ -n "$BOING_BAG_2" && -f "$BOING_BAG_2" ]]; then
    echo "==> Extracting Boing Bag 2: $(basename "$BOING_BAG_2")"
    extract_lha_to_host "$BOING_BAG_2" "$SYS_HOST"
  fi

  for bb_dir in "$SYS_HOST"/BoingBag* "$SYS_HOST"/boingbag*; do
    if [[ -d "$bb_dir" ]]; then
      cp -R "$bb_dir"/* "$SYS_HOST/" 2>/dev/null || true
      rm -rf "$bb_dir"
    fi
  done

  DETECTED_WB_VERSION="3.9"
  echo "==> Workbench version detected: 3.9 (from OS 3.9 CD ISO structure)"

  if [[ -f "$SYS_HOST/L/FastFileSystem" ]]; then
    FFS_PATH="$SYS_HOST/L/FastFileSystem"
    echo "==> FFS found in OS 3.9 CD: $FFS_PATH"
  fi

  OS32_MEDIA=0

else
  # --- Standard ADF-based extraction ---
  while IFS= read -r f; do
    [[ -n "$f" ]] && ADFS+=("$f")
  done < <(find "$ADF_SRC" -type f \( -name '*.adf' -o -name '*.ADF' \) -print 2>/dev/null | LC_ALL=C sort || true)

  if [[ ${#ADFS[@]} -eq 0 ]]; then
    die "No .adf files found in: $ADF_SRC"
  fi

  declare -a LOCALE_ADFS
  declare -a LOCALE_CANDIDATES
  LOCALE_SKIPPED=0

  for a in "${ADFS[@]}"; do
    bn="$(basename "$a")"
    bn_lower="$(echo "$bn" | tr '[:upper:]' '[:lower:]')"
    locale_code=""
    is_locale_adf=0
    if locale_code="$(detect_locale_code_from_adf_name "$bn_lower")"; then
      is_locale_adf=1
    elif echo "$bn_lower" | grep -qE '^locale'; then
      is_locale_adf=1
    fi

    if [[ "$is_locale_adf" -eq 1 ]]; then
      LOCALE_CANDIDATES+=("$a")
      if [[ -z "$LOCALES" ]]; then
        LOCALE_ADFS+=("$a")
        BASE_ADFS+=("$a")
      else
        if [[ -n "$locale_code" ]] && echo ",$LOCALES," | grep -qi ",$locale_code,"; then
          LOCALE_ADFS+=("$a")
          BASE_ADFS+=("$a")
        elif is_base_locale_adf_name "$bn_lower"; then
          echo "==> Including base Locale ADF (shared Countries): $bn"
          LOCALE_ADFS+=("$a")
          BASE_ADFS+=("$a")
        elif [[ -z "$locale_code" ]]; then
          echo "==> WARN: Locale ADF naming not recognized; skipping: $bn"
          LOCALE_SKIPPED=$((LOCALE_SKIPPED + 1))
        else
          echo "==> Skipping locale ADF (not selected): $bn"
          LOCALE_SKIPPED=$((LOCALE_SKIPPED + 1))
        fi
      fi
    elif echo "$bn_lower" | grep -qE '^modules'; then
      echo "==> Skipping Modules ADF from bulk extraction: $bn (handled separately)"
    elif echo "$bn" | grep -Ei '(update|hotfix|patch)' >/dev/null 2>&1; then
      PATCH_ADFS+=("$a")
    elif is_glowicons_adf_name "$bn_lower"; then
      ICONGLOW_ADFS+=("$a")
    elif is_4coloricons_adf_name "$bn_lower"; then
      ICON4_ADFS+=("$a")
    else
      BASE_ADFS+=("$a")
    fi
  done

  if [[ -n "$LOCALES" && ${#LOCALE_ADFS[@]} -eq 0 && ${#LOCALE_CANDIDATES[@]} -gt 0 ]]; then
    echo "==> WARN: No locale ADFs matched filter '$LOCALES'; including all locale ADFs as fallback."
    LOCALE_ADFS=( "${LOCALE_CANDIDATES[@]}" )
    BASE_ADFS+=( "${LOCALE_CANDIDATES[@]}" )
  fi

  if [[ "$ICON_SET" == "glowicons" ]]; then
    if [[ ${#ICONGLOW_ADFS[@]} -gt 0 ]]; then
      SELECTED_ICONSET_ADFS=( "${ICONGLOW_ADFS[@]}" )
    elif [[ ${#ICON4_ADFS[@]} -gt 0 ]]; then
      SELECTED_ICONSET_ADFS=( "${ICON4_ADFS[@]}" )
      echo "==> WARN: GlowIcons selected but no GlowIcons ADF found; falling back to 4ColorIcons."
    fi
  else
    if [[ ${#ICON4_ADFS[@]} -gt 0 ]]; then
      SELECTED_ICONSET_ADFS=( "${ICON4_ADFS[@]}" )
    elif [[ ${#ICONGLOW_ADFS[@]} -gt 0 ]]; then
      SELECTED_ICONSET_ADFS=( "${ICONGLOW_ADFS[@]}" )
      echo "==> WARN: Standard icons selected but no 4ColorIcons ADF found; falling back to GlowIcons."
    fi
  fi

  if [[ ${#SELECTED_ICONSET_ADFS[@]} -gt 0 ]]; then
    BASE_ADFS+=( "${SELECTED_ICONSET_ADFS[@]}" )
    for _icon_adf in "${SELECTED_ICONSET_ADFS[@]}"; do
      echo "==> Icon set ADF selected: $(basename "$_icon_adf")"
    done
  else
    echo "==> WARN: No icon set ADF found (neither 4ColorIcons nor GlowIcons)."
  fi

  echo "==> ADFs: total=${#ADFS[@]} base=${#BASE_ADFS[@]} patch=${#PATCH_ADFS[@]} locale=${#LOCALE_ADFS[@]} icon4=${#ICON4_ADFS[@]} glow=${#ICONGLOW_ADFS[@]} selectedIcons=${#SELECTED_ICONSET_ADFS[@]} skipped=$LOCALE_SKIPPED"

  OS32_MEDIA=0
  for a in "${ADFS[@]}"; do
    if echo "$(basename "$a")" | grep -qiE '3\.2'; then
      OS32_MEDIA=1
      break
    fi
  done

  for wb in "$ADF_SRC"/Workbench*.adf "$ADF_SRC"/Workbench*.ADF; do
    [[ -f "$wb" ]] && { WORKBENCH_ADF="$wb"; break; }
  done
  detect_workbench_version

  # Extract base ADFs in parallel, locale + icon ADFs sequentially after.
  _base_parallel=()
  for _ba in "${BASE_ADFS[@]}"; do
    _is_special=0
    for _ia in "${SELECTED_ICONSET_ADFS[@]}"; do
      [[ "$_ba" == "$_ia" ]] && { _is_special=1; break; }
    done
    if [[ $_is_special -eq 0 ]]; then
      for _la in "${LOCALE_ADFS[@]}"; do
        [[ "$_ba" == "$_la" ]] && { _is_special=1; break; }
      done
    fi
    [[ $_is_special -eq 0 ]] && _base_parallel+=("$_ba")
  done

  if [[ ${#_base_parallel[@]} -gt 0 ]]; then
    # Sequential extraction — reliable, no race conditions.
    for _seq_adf in "${_base_parallel[@]}"; do
      extract_adf_to_host "$_seq_adf" "$SYS_HOST"
    done
  fi

  if [[ ${#LOCALE_ADFS[@]} -gt 0 ]]; then
    echo "==> Extracting ${#LOCALE_ADFS[@]} locale ADF(s) sequentially"
    for a in "${LOCALE_ADFS[@]}"; do
      extract_adf_to_host "$a" "$SYS_HOST"
    done
  fi

  if [[ ${#SELECTED_ICONSET_ADFS[@]} -gt 0 ]]; then
    echo "==> Extracting ${#SELECTED_ICONSET_ADFS[@]} icon ADF(s) sequentially"
    for a in "${SELECTED_ICONSET_ADFS[@]}"; do
      extract_adf_to_host "$a" "$SYS_HOST"
    done
  fi

  if [[ ${#PATCH_ADFS[@]} -gt 0 ]]; then
    echo "==> Extracting ${#PATCH_ADFS[@]} patch ADF(s) sequentially"
    for a in "${PATCH_ADFS[@]}"; do
      extract_adf_to_host "$a" "$SYS_HOST"
    done
  fi

fi  # end IS_39_ISO if/else

# Nested locale ADFs from Install drawer
if [[ -d "$SYS_HOST/Install" ]]; then
  _nested_locale_count=0
  for _nadf in "$SYS_HOST"/Install/Locale*.adf "$SYS_HOST"/Install/Locale*.ADF; do
    [[ -f "$_nadf" ]] || continue
    _nbn="$(basename "$_nadf")"
    _ncode="$(detect_locale_code_from_adf_name "$_nbn")"
    _extract_nested=0
    if [[ -z "$LOCALES" ]]; then
      _extract_nested=1
    elif [[ -n "$_ncode" ]] && echo ",$LOCALES," | grep -qi ",$_ncode,"; then
      _extract_nested=1
    fi
    if [[ "$_extract_nested" -eq 1 ]]; then
      echo "  -> extracting nested locale ADF: $_nbn"
      extract_adf_to_host "$_nadf" "$SYS_HOST"
      _nested_locale_count=$((_nested_locale_count + 1))
    fi
  done
  [[ "$_nested_locale_count" -gt 0 ]] && echo "==> Nested locale ADFs extracted: $_nested_locale_count"
fi

# Locale fallback from ISO root
if [[ -d "$ISO_ROOT/Locale" ]]; then
  mkdir -p "$SYS_HOST/Locale" >/dev/null 2>&1 || true
  cp -R "$ISO_ROOT/Locale/"* "$SYS_HOST/Locale/" 2>/dev/null || true
fi

# Normalize Locale drawers (merge nested Locale/ dirs + root-level Catalogs/Help/Languages/Countries)
normalize_locale_drawers "$SYS_HOST"

# Decompress .Z files (OS 3.2.x update media)
decompress_amiga_z_files() {
  local root="$1"
  command -v uncompress >/dev/null 2>&1 || return 0
  [[ -d "$root" ]] || return 0
  local count=0
  while IFS= read -r -d '' zfile; do
    uncompress -f "$zfile" >/dev/null 2>&1 && count=$((count + 1))
  done < <(find "$root" -type f -iname "*.Z" -print0 2>/dev/null)
  [[ "$count" -gt 0 ]] && echo "==> Decompressed $count .Z files"
}

if [[ "${OS32_MEDIA:-0}" -eq 1 ]]; then
  decompress_amiga_z_files "$SYS_HOST"
fi

# Move WBStartup content to Disabled (Windows parity)
if [[ -d "$SYS_HOST/WBStartup" ]]; then
  mkdir -p "$SYS_HOST/WBStartup/Disabled"
  while IFS= read -r -d '' _wb_item; do
    _wb_name="$(basename "$_wb_item")"
    case "$_wb_name" in
      Disabled|OnetimeRunWB|OnetimeRunWB.info|OneTimeRunWB|OneTimeRunWB.info) continue ;;
    esac
    mv "$_wb_item" "$SYS_HOST/WBStartup/Disabled/" 2>/dev/null || true
  done < <(find "$SYS_HOST/WBStartup" -mindepth 1 -maxdepth 1 -print0)
fi

# Remove FIRSTTIMEBOOT flags from ADF Env-Archive
for _ftb in FIRSTTIMEBOOT FIRSTTIMEBOOTWB REBOOT; do
  rm -f "$SYS_HOST/Prefs/Env-Archive/$_ftb" 2>/dev/null || true
done

# Save backdrop IFF files BEFORE sanitization removes them from root
_bd_stash="$STAGE/backdrop-stash"
mkdir -p "$_bd_stash"
for _iff in default_pal.iff default_ntsc.iff default.pal.iff default.ntsc.iff pattern.iff; do
  [[ -f "$SYS_HOST/$_iff" ]] && cp -f "$SYS_HOST/$_iff" "$_bd_stash/$_iff"
done
for _iff in default_pal.iff default_ntsc.iff pattern.iff; do
  [[ -f "$SYS_HOST/Prefs/Presets/Backdrops/$_iff" ]] && cp -f "$SYS_HOST/Prefs/Presets/Backdrops/$_iff" "$_bd_stash/$_iff"
  [[ -f "$SYS_HOST/Prefs/Presets/$_iff" ]] && cp -f "$SYS_HOST/Prefs/Presets/$_iff" "$_bd_stash/$_iff"
done

# Classic RTG builds should stage the same resolution-matched backdrop PNG as
# PiStorm before SYS_HOST is merged into the image. Native Classic builds keep
# the stock IFF preset path instead.
if [[ -n "$RTG_CARD" && "$RTG_CARD" != "none" ]]; then
  install_rtg_backdrop_png "$SYS_HOST" "${ADF_SRC:-}" "$SCREEN_WIDTH" "$SCREEN_HEIGHT" "$DETECTED_WB_VERSION"
fi

# Extract Modules ADF for the target hardware. WB 3.2+ requires L/System-Startup
# from the matching Modules disk to boot on older Kickstart ROMs (LoadModule ROMUPDATE).
# Without this, the Startup-Sequence halts with "This disk must be booted from
# Kickstart ROM 3.2 (V47) or from the 'Modules' disk".
# Select the Modules ADF matching the effective WB version.
# When Update ADFs are applied (e.g. Update3.2.3.adf), the effective version
# is higher than the base Workbench ADF. Use the HIGHEST available version.
_modules_adf=""
_wb_ver="${DETECTED_WB_VERSION:-3.2}"
# Check if an Update ADF bumps the version (e.g. Update3.2.3 → 3.2.3)
for _upd in "$ADF_SRC"/Update*.adf "$ADF_SRC"/update*.adf; do
  [[ -f "$_upd" ]] || continue
  _upd_bn="$(basename "$_upd" .adf)"
  _upd_ver="$(echo "$_upd_bn" | sed -E 's/^[Uu]pdate//')"
  if [[ -n "$_upd_ver" ]]; then
    _wb_ver="$_upd_ver"
    echo "==> Classic: Effective WB version bumped to $_wb_ver (from $(basename "$_upd"))"
  fi
done
# Select Modules ADF: prefer machine-specific match, then fall back.
# Map CLASSIC_MACHINE to Modules ADF prefix.
_mod_prefix=""
case "${CLASSIC_MACHINE:-}" in
  A500)   _mod_prefix="ModulesA500" ;;
  A600)   _mod_prefix="ModulesA600" ;;
  A1200)  _mod_prefix="ModulesA1200" ;;
  A2000)  _mod_prefix="ModulesA2000" ;;
  A3000)  _mod_prefix="ModulesA3000" ;;
  A4000)  _mod_prefix="ModulesA4000" ;;
  A4000T) _mod_prefix="ModulesA4000T" ;;
  CD32)   _mod_prefix="ModulesCD32" ;;
  *)      _mod_prefix="ModulesA1200" ;;  # default fallback
esac
echo "==> Classic: Modules ADF search prefix: $_mod_prefix (machine: ${CLASSIC_MACHINE:-<default>})"
for _mod_try in \
  "$ADF_SRC"/${_mod_prefix}_${_wb_ver}.adf \
  "$ADF_SRC"/${_mod_prefix}_${_wb_ver}*.adf \
  "$ADF_SRC"/${_mod_prefix}_*.adf \
  "$ADF_SRC"/${_mod_prefix}*.adf \
  "$ADF_SRC"/Modules*.adf \
  ; do
  [[ -f "$_mod_try" ]] && { _modules_adf="$_mod_try"; break; }
done
if [[ -n "$_modules_adf" ]]; then
  echo "==> Classic: Extracting Modules ADF: $(basename "$_modules_adf")"
  # Extract to a SEPARATE temp directory — NOT directly to $SYS_HOST.
  # The Modules ADF contains its own Startup-Sequence, Shell.info, ReadMe etc.
  # that would overwrite the Workbench versions. Module files are copied to the
  # image AFTER extract_host_to_sys (see below) with .uaem sidecar files that
  # preserve Amiga protection bits (Pure bit on L/System-startup for ROMUPDATE).
  #
  # IMPORTANT: Extract with hst-imager, then create .uaem sidecars from the ADF's
  # protection bits. hst-imager --uaemetadata UaeMetafile reads per-file .uaem sidecar
  # files (not _UAEFSDB.___) when doing single-file copies.  Without .uaem files the
  # Pure bit on L/System-startup is silently dropped and LoadModule ROMUPDATE refuses
  # to load it (Pure bit = reentrant/position-independent code requirement for LoadModule).
  _modules_extract="$STAGE/modules-adf-extract"
  rm -rf "$_modules_extract" 2>/dev/null || true
  mkdir -p "$_modules_extract"
  "$HST_IMAGER" fs extract "$_modules_adf" "$_modules_extract" --recursive --force \
    || "$HST_IMAGER" fs extract "$_modules_adf" "$_modules_extract" --recursive \
    || die "Failed to extract Modules ADF with hst-imager: $_modules_adf"
  chmod -R u+rwX "$_modules_extract" 2>/dev/null || true

  # Create .uaem sidecar files (hst-imager UaeMetafile format) so --uaemetadata
  # UaeMetafile picks up the original Amiga protection bits from the ADF.
  # Queries each directory in the ADF; converts UPPERCASE attr letters to lowercase
  # (.uaem format: lowercase = bit set; dash = bit not set).
  _mod_write_uaem() {
    local _adf="$1" _xdir="$2" _subdir="$3"
    local _hdir="$_xdir/$_subdir"
    [[ -d "$_hdir" ]] || return 0
    local _ts _name _attrs _uaem
    _ts="$(date '+%Y-%m-%d %H:%M:%S.00')"
    while IFS='|' read -r _col1 _col2 _col3 _col4 _rest; do
      _name="${_col1//[[:space:]]/}"
      _attrs="${_col4//[[:space:]]/}"
      [[ -z "$_name" || "$_name" == "Name" || "$_name" =~ ^-+ || "$_name" =~ ^\< ]] && continue
      [[ -f "$_hdir/$_name" ]] || continue
      [[ "$_attrs" == "----RWED" ]] && continue
      _uaem="$(printf '%s' "$_attrs" | tr '[:upper:]' '[:lower:]')"
      printf '%s %s \n' "$_uaem" "$_ts" > "$_hdir/$_name.uaem"
    done < <("$HST_IMAGER" fs dir "$_adf/$_subdir" 2>/dev/null | grep '|')
  }
  _mod_write_uaem "$_modules_adf" "$_modules_extract" "L"
  _mod_write_uaem "$_modules_adf" "$_modules_extract" "C"
  _mod_write_uaem "$_modules_adf" "$_modules_extract" "LIBS"
  _mod_write_uaem "$_modules_adf" "$_modules_extract" "LIBS/Modules"
  _mod_write_uaem "$_modules_adf" "$_modules_extract" "LIBS/Resources"
  for _mod_sub in "$_modules_extract"/LIBS/*/; do
    [[ -d "$_mod_sub" ]] || continue
    _mod_sub_name="$(basename "$_mod_sub")"
    case "$_mod_sub_name" in Modules|Resources) continue ;; esac
    _mod_write_uaem "$_modules_adf" "$_modules_extract" "LIBS/$_mod_sub_name"
  done
  for _mod_sub in "$_modules_extract"/DEVS/*/; do
    [[ -d "$_mod_sub" ]] || continue
    _mod_write_uaem "$_modules_adf" "$_modules_extract" "DEVS/$(basename "$_mod_sub")"
  done

  _MODULES_INSTALLED=1
  echo "==> Classic: Modules ADF extracted + .uaem protection sidecars written"
else
  echo "==> WARN: No Modules ADF found — WB 3.2 may not boot on Kickstart < V47"
fi

# Sanitize root: remove Install/Extras ADF junk
echo "==> Sanitizing SYS_HOST root"
_rm_count=0
# Preserve HDToolBox binary in Tools/ before sanitization removes HDTools/.
# Only copy the BINARY — do NOT copy the .info icon. The icon set ADF
# (GlowIcons/4ColorIcons) already placed the proper icon at Tools/HDToolBox.info.
# Overwriting it with the HDSetup ADF's generic icon would lose the styled version.
if [[ ! -f "$SYS_HOST/Tools/HDToolBox" ]]; then
  mkdir -p "$SYS_HOST/Tools"
  for _hdtb_src in "$SYS_HOST/HDToolBox" "$SYS_HOST/HDTools/HDToolBox"; do
    if [[ -f "$_hdtb_src" ]]; then
      cp -f "$_hdtb_src" "$SYS_HOST/Tools/HDToolBox"
      echo "==> Preserved HDToolBox binary from $(dirname $_hdtb_src)/ to Tools/"
      break
    fi
  done
  # Only copy .info if none exists (icon set ADF didn't provide one)
  if [[ ! -f "$SYS_HOST/Tools/HDToolBox.info" ]]; then
    for _hdtb_src in "$SYS_HOST/HDToolBox.info" "$SYS_HOST/HDTools/HDToolBox.info"; do
      if [[ -f "$_hdtb_src" ]]; then
        cp -f "$_hdtb_src" "$SYS_HOST/Tools/HDToolBox.info"
        break
      fi
    done
  fi
fi
for _junk in ActivateCDROM ActivateCDROM.info HDSetup HDSetup.info HDTools HDTools.info \
  Install.info SYStoInstallDisk SYStoInstallDisk.info CI CI.info; do
  [[ -e "$SYS_HOST/$_junk" ]] && { rm -rf "$SYS_HOST/$_junk"; _rm_count=$((_rm_count+1)); }
done
# Move root-level Installer binary to System/Installer (conventional location).
# WB 3.2 install media places Installer at SYS: root; relocate it so the
# INSTALLER: assign can consistently target SYS:System.
if [[ -f "$SYS_HOST/Installer" ]] && [[ ! -f "$SYS_HOST/System/Installer" ]]; then
  mkdir -p "$SYS_HOST/System"
  mv "$SYS_HOST/Installer" "$SYS_HOST/System/Installer"
  [[ -f "$SYS_HOST/Installer.info" ]] && mv "$SYS_HOST/Installer.info" "$SYS_HOST/System/Installer.info"
  echo "==> Relocated Installer → System/Installer"
fi
for _bg in "$SYS_HOST"/background_*.png "$SYS_HOST"/Background_*.png; do
  [[ -f "$_bg" ]] && { rm -f "$_bg"; _rm_count=$((_rm_count+1)); }
done
for _iff in default.ntsc.iff default.pal.iff default_ntsc.iff default_pal.iff pattern.iff; do
  [[ -f "$SYS_HOST/$_iff" ]] && { rm -f "$SYS_HOST/$_iff"; _rm_count=$((_rm_count+1)); }
done
for _root_tool in CLI CLI.info Shell Shell.info Format Format.info Font Font.info; do
  [[ -f "$SYS_HOST/$_root_tool" ]] && { rm -f "$SYS_HOST/$_root_tool"; _rm_count=$((_rm_count+1)); }
  [[ -d "$SYS_HOST/$_root_tool" ]] && { rm -rf "$SYS_HOST/$_root_tool"; _rm_count=$((_rm_count+1)); }
done
# Remove Devs subdirectories duplicated at root (should be accessed via assigns like
# MONITORS: → DEVS:Monitors, not as SYS: root directories).
# Important: do NOT remove root Keymaps here. Some Classic media still stage
# additional keyboard layouts at SYS:Keymaps/, and the reorganization pass
# below must merge those into Devs/Keymaps before the root drawer is deleted.
for _devs_dup in DOSDrivers Monitors Printers DataTypes; do
  if [[ -d "$SYS_HOST/$_devs_dup" && -d "$SYS_HOST/Devs/$_devs_dup" ]]; then
    rm -rf "$SYS_HOST/$_devs_dup"
    _rm_count=$((_rm_count+1))
  fi
  # Also remove the .info shortcuts — they clutter the root on Classic machines
  # (the assigns handle access; icons aren't needed)
  [[ -f "$SYS_HOST/${_devs_dup}.info" ]] && { rm -f "$SYS_HOST/${_devs_dup}.info"; _rm_count=$((_rm_count+1)); }
done
# Raw .adf files at SYS: root (from ADF extraction — contents already merged)
for _radf in "$SYS_HOST"/*.adf "$SYS_HOST"/*.ADF; do
  [[ -f "$_radf" ]] && { rm -f "$_radf"; _rm_count=$((_rm_count+1)); }
done
for _radfi in "$SYS_HOST"/*.adf.info "$SYS_HOST"/*.ADF.info; do
  [[ -f "$_radfi" ]] && { rm -f "$_radfi"; _rm_count=$((_rm_count+1)); }
done
# Install/ directory contains raw ADFs from install media — not needed on final image
if [[ -d "$SYS_HOST/Install" ]]; then
  rm -rf "$SYS_HOST/Install"
  _rm_count=$((_rm_count+1))
fi
[[ -f "$SYS_HOST/Install.info" ]] && { rm -f "$SYS_HOST/Install.info"; _rm_count=$((_rm_count+1)); }
for _bad_dir in Programs WBSetup "Installer Extras"; do
  [[ -d "$SYS_HOST/$_bad_dir" ]] && { rm -rf "$SYS_HOST/$_bad_dir"; _rm_count=$((_rm_count+1)); }
done
echo "==> Removed $_rm_count non-Workbench items from SYS_HOST root"

# Reorganize fonts
_font_files=()
while IFS= read -r _ff; do
  [[ -n "$_ff" ]] && _font_files+=("$_ff")
done < <(find "$SYS_HOST" -maxdepth 1 -name '*.font' -type f 2>/dev/null)
if [[ ${#_font_files[@]} -gt 0 ]]; then
  mkdir -p "$SYS_HOST/Fonts"
  for _ff in "${_font_files[@]}"; do
    _fname="$(basename "$_ff")"
    _stem="${_fname%.font}"
    mv "$_ff" "$SYS_HOST/Fonts/$_fname"
    [[ -d "$SYS_HOST/$_stem" ]] && mv "$SYS_HOST/$_stem" "$SYS_HOST/Fonts/$_stem"
  done
fi

# Reorganize keymaps — AmigaOS 3.1 places them at ADF root (Keymaps/),
# AmigaOS 3.2 and some install media use Storage/Keymaps/. Search all known
# locations and merge into Devs/Keymaps/ so Prefs/Input can enumerate them.
_km_count=0
mkdir -p "$SYS_HOST/Devs/Keymaps"
for _km_src_dir in \
    "$SYS_HOST/Keymaps" \
    "$SYS_HOST/Storage/Keymaps" \
    "$SYS_HOST/Extras/Keymaps" \
    "$SYS_HOST/System/Keymaps"; do
  [[ -d "$_km_src_dir" ]] || continue
  for _km in "$_km_src_dir"/*; do
    [[ -e "$_km" ]] || continue
    [[ -f "$_km" ]] || continue
    _km_name="$(basename "$_km")"
    if [[ ! -e "$SYS_HOST/Devs/Keymaps/$_km_name" ]]; then
      cp "$_km" "$SYS_HOST/Devs/Keymaps/$_km_name"
      _km_count=$((_km_count + 1))
    fi
  done
  rm -rf "$_km_src_dir"
done
if [[ "$_km_count" -gt 0 ]]; then
  echo "==> Keymaps reorganized: $_km_count keymap(s) merged into Devs/Keymaps/"
fi
# Keymaps should not remain visible at SYS: root on the final Classic image.
# After the merge pass, remove any leftover root drawer/icon shortcut.
rm -rf "$SYS_HOST/Keymaps" >/dev/null 2>&1 || true
rm -f "$SYS_HOST/Keymaps.info" >/dev/null 2>&1 || true

# Fix A (host-side): ensure core Workbench paths are FILES, not drawers
if [[ "$IS_39_ISO" -eq 1 ]]; then
  echo "==> Fix A: skipped (OS 3.9 CD)"
elif [[ -n "$WORKBENCH_ADF" ]]; then
  fix_sys_host_file_collisions_from_workbench_adf "$WORKBENCH_ADF" "$SYS_HOST"
else
  echo "==> Fix A: Workbench ADF not found (skipping)"
fi

fix_sys_host_file_collisions_from_iso "$ISO_ROOT" "$SYS_HOST"
canonicalize_sys_host_core_paths "$SYS_HOST" "$ISO_ROOT"

# Remove IconUpdate drawer
rm -rf "$SYS_HOST/IconUpdate" "$SYS_HOST/IconUpdate.info" >/dev/null 2>&1 || true
find "$SYS_HOST" -maxdepth 2 -type d -name "IconUpdate" -exec rm -rf {} + >/dev/null 2>&1 || true

# Set all Color Icons borderless before merging into image
set_coloricons_borderless_host "$SYS_HOST"

# Sanitize non-ASCII filenames for FFS compatibility
echo "==> Sanitizing non-ASCII filenames in SYS_HOST for FFS compatibility"
sanitize_non_ascii_filenames "$SYS_HOST"

# Vampire SAGA uses an Apollo 68080, not a standard 68030/040/060 MMU target.
# Keep the Workbench merge path unchanged for other Classic builds, but strip
# the MMULibs payload from the staged SYS: tree for Vampire so neither the
# library nor the Mu* tools land on the final image.
if [[ "$RTG_CARD" == "VampireSAGA" ]]; then
  _vamp_mmu_removed=0
  while IFS= read -r -d '' _mmu_cmd; do
    rm -f "$_mmu_cmd"
    _vamp_mmu_removed=$((_vamp_mmu_removed + 1))
  done < <(find "$SYS_HOST/C" -maxdepth 1 -type f -name 'Mu*' -print0 2>/dev/null)
  if [[ -f "$SYS_HOST/Libs/mmu.library" ]]; then
    rm -f "$SYS_HOST/Libs/mmu.library"
    _vamp_mmu_removed=$((_vamp_mmu_removed + 1))
  fi
  if [[ -d "$SYS_HOST/Locale/Help/MMUlib" ]]; then
    rm -rf "$SYS_HOST/Locale/Help/MMUlib"
    _vamp_mmu_removed=$((_vamp_mmu_removed + 1))
  fi
  echo "==> Classic: Removed MMULibs payload for Vampire SAGA ($_vamp_mmu_removed item(s))"
fi

extract_host_to_sys "$SYS_HOST" "$SYS_ROOT"

# --- Install Modules ADF files directly to image (with Amiga protection bits) ---
# This runs AFTER extract_host_to_sys so the Workbench ADF versions are already on
# the image. We overwrite specific files with V47 versions from the Modules ADF.
# Using fs_copy_file (hst-imager) preserves _UAEFSDB.___ metadata including the
# Pure (P) protection bit that LoadModule ROMUPDATE requires.
#
# The Modules ADF ships machine-specific files under subdirectories:
#   LIBS/{machine}/exec.library  — machine-specific V47 exec (required by ROMUPDATE)
#   DEVS/{machine}/scsi.device   — machine-specific disk controller
# These must be installed to LIBS: and DEVS: (flattened, not in subdirs).
# Without exec.library, ROMUPDATE loads V47 System-Startup but exec stays at
# V40/V46, causing Guru 8700 0000 (Address Error) after warm reset.
if [[ "${_MODULES_INSTALLED:-0}" -eq 1 && -d "${_modules_extract:-}" ]]; then
  _mod_count=0

  # All Modules files must be copied with hst-imager + --uaemetadata UaeMetafile,
  # bypassing DISK_ENGINE routing. amiga-tools disk fs copy does not read _UAEFSDB.___
  # sidecar files, so the Pure bit on L/System-startup (required by LoadModule ROMUPDATE)
  # would be silently dropped. hst-imager reads the sidecar files written during
  # hst-imager extraction and applies the original Amiga protection bits.
  _mod_hst_copy() {
    local src="$1" dst="$2"
    ensure_img_dir "$(dirname "$dst")"
    "$HST_IMAGER" fs copy "$src" "$dst" --uaemetadata UaeMetafile --force 2>/dev/null \
      || "$HST_IMAGER" fs copy "$src" "$dst" --uaemetadata UaeMetafile \
      || "$HST_IMAGER" fs copy "$src" "$dst" --force
  }

  # L/ handlers — System-Startup + supporting handlers
  for _mf in \
    L/System-startup L/System-Startup \
    L/Shell-Seg L/Ram-Handler \
    L/Con-Handler L/FastFileSystem L/Syslog \
    ; do
    if [[ -f "$_modules_extract/$_mf" ]]; then
      _mod_hst_copy "$_modules_extract/$_mf" "$SYS_ROOT/$_mf"
      _mod_count=$((_mod_count + 1))
    fi
  done

  # C/ — only LoadModule (other C/ commands come from Workbench/Update ADF)
  if [[ -f "$_modules_extract/C/LoadModule" ]]; then
    _mod_hst_copy "$_modules_extract/C/LoadModule" "$SYS_ROOT/C/LoadModule"
    _mod_count=$((_mod_count + 1))
  fi

  # LIBS/ — ROM-resident libraries (DOWNGRADE refs + ROMUPDATE scan targets)
  for _mf in dos.library gadtools.library graphics.library intuition.library \
    icon.library layers.library utility.library version.library workbench.library \
    mathffp.library mathieeesingbas.library; do
    if [[ -f "$_modules_extract/LIBS/$_mf" ]]; then
      _mod_hst_copy "$_modules_extract/LIBS/$_mf" "$SYS_ROOT/Libs/$_mf"
      _mod_count=$((_mod_count + 1))
    fi
  done

  # LIBS/Modules/ (bootmenu + syscheck)
  for _mf in bootmenu syscheck; do
    if [[ -f "$_modules_extract/LIBS/Modules/$_mf" ]]; then
      ensure_img_dir "$SYS_ROOT/Libs/Modules"
      _mod_hst_copy "$_modules_extract/LIBS/Modules/$_mf" "$SYS_ROOT/Libs/Modules/$_mf"
      _mod_count=$((_mod_count + 1))
    fi
  done

  # LIBS/Resources/ — ROM-resident resources
  for _mf in FileSystem.resource battclock.resource card.resource disk.resource; do
    if [[ -f "$_modules_extract/LIBS/Resources/$_mf" ]]; then
      ensure_img_dir "$SYS_ROOT/Libs/Resources"
      _mod_hst_copy "$_modules_extract/LIBS/Resources/$_mf" "$SYS_ROOT/Libs/Resources/$_mf"
      _mod_count=$((_mod_count + 1))
    fi
  done

  # Machine-specific exec.library: LIBS/{machine}/exec.library → Libs/exec.library
  # Critical for ROMUPDATE — without this, V47 System-Startup crashes on V40/V46 exec.
  # Multi-variant ADFs (A500 has A500+A1000, A2000 has A2000A+A2000B) ship identical
  # binaries, so picking the first match is safe.
  _exec_src=""
  for _exec_cand in "$_modules_extract"/LIBS/*/exec.library; do
    [[ -f "$_exec_cand" ]] && { _exec_src="$_exec_cand"; break; }
  done
  if [[ -n "$_exec_src" ]]; then
    _mod_hst_copy "$_exec_src" "$SYS_ROOT/Libs/exec.library"
    echo "==> Classic: Installed exec.library from Modules ADF ($(basename "$(dirname "$_exec_src")")/ variant)"
    _mod_count=$((_mod_count + 1))
  fi

  # Machine-specific scsi.device: DEVS/{machine}/scsi.device → Devs/scsi.device
  # Not all machines have this (A500/A2000 don't).
  _scsi_src=""
  for _scsi_cand in "$_modules_extract"/DEVS/*/scsi.device; do
    [[ -f "$_scsi_cand" ]] && { _scsi_src="$_scsi_cand"; break; }
  done
  if [[ -n "$_scsi_src" ]]; then
    _mod_hst_copy "$_scsi_src" "$SYS_ROOT/Devs/scsi.device"
    echo "==> Classic: Installed scsi.device from Modules ADF ($(basename "$(dirname "$_scsi_src")")/ variant)"
    _mod_count=$((_mod_count + 1))
  fi

  echo "==> Classic: Installed $_mod_count module files from Modules ADF (with protection bits via hst-imager)"
  rm -rf "$_modules_extract" 2>/dev/null || true
fi

# Copy backdrop presets directly to image (Backdrops ADF often not extracted)
ensure_img_dir "$SYS_ROOT/Prefs/Presets/Backdrops"
_bd_copied=0
_bd_adf=""
for _bd_try in "$ADF_SRC"/Backdrops*.adf "$ADF_SRC"/backdrops*.adf "$ADF_SRC"/Backdrops*.ADF; do
  [[ -f "$_bd_try" ]] && { _bd_adf="$_bd_try"; break; }
done
if [[ -n "$_bd_adf" ]]; then
  _bd_extract="$STAGE/backdrops-adf-extract"
  rm -rf "$_bd_extract" 2>/dev/null || true
  mkdir -p "$_bd_extract"
  echo "==> Extracting Backdrops ADF: $(basename "$_bd_adf")"
  extract_adf_to_host "$_bd_adf" "$_bd_extract" 2>/dev/null || true
fi
for _bd_name in default_pal.iff default_ntsc.iff pattern.iff; do
  _bd_src=""
  for _bd_cand in \
    "$_bd_stash/$_bd_name" \
    "${_bd_extract:-/nonexistent}"/"$_bd_name" \
    "${_bd_extract:-/nonexistent}"/Prefs/Presets/Backdrops/"$_bd_name" \
    "$SYS_HOST/Prefs/Presets/Backdrops/$_bd_name" \
    "$SYS_HOST/$_bd_name" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/$_bd_name" \
    ; do
    [[ -f "$_bd_cand" ]] && { _bd_src="$_bd_cand"; break; }
  done
  if [[ -n "$_bd_src" ]]; then
    fs_copy_file "$_bd_src" "$SYS_ROOT/Prefs/Presets/Backdrops/$_bd_name"
    _bd_copied=$((_bd_copied + 1))
  fi
done
rm -rf "${_bd_extract:-}" 2>/dev/null || true
rm -rf "$_bd_stash" 2>/dev/null || true
[[ "$_bd_copied" -gt 0 ]] && echo "==> Backdrop presets: $_bd_copied files copied to image"

# Ensure FIRSTTIMEBOOT flags exist
ensure_firsttimeboot_flags_true "$SYS_ROOT"


# -----------------------------------------------------------------------------
# 5) Overlay additions from Assets (non-destructive)
# -----------------------------------------------------------------------------
echo "==> PROGRESS 5/10 Overlaying Assets"
echo "==> 5) Overlay additions from Assets (non-destructive)"
SYS_PAYLOAD="$ASSETS_DIR/AmigaFiles/System"
if [[ ! -d "$SYS_PAYLOAD" ]]; then
  die "Missing System payload in Assets: $SYS_PAYLOAD"
fi

# --- sanity: base OS drawers should exist after ADF merge ---
for _p in \
  "$SYS_ROOT/C" \
  "$SYS_ROOT/Devs" \
  "$SYS_ROOT/Libs" \
  "$SYS_ROOT/S" \
  "$SYS_ROOT/System" \
  "$SYS_ROOT/Tools" \
  "$SYS_ROOT/Utilities" \
  "$SYS_ROOT/Prefs" \
  "$SYS_ROOT/Fonts" \
  ; do
  if ! fs_dir "$_p"; then
    echo "==> WARN: Expected base drawer missing after ADF merge: $_p" >&2
  fi
done

# --- 5a) Overlay S: startup scripts, user-startup, etc. (file-by-file) ---
if [[ -d "$SYS_PAYLOAD/S" ]]; then
  echo "==> Overlay: System/S -> SYS:S (file-by-file)"
  ensure_img_dir "$SYS_ROOT/S"
  while IFS= read -r -d '' item; do
    base="$(basename "$item")"
    case "$base" in
      Startup-Sequence|startup-sequence)
        echo "==> NOTE: Skipping Assets S/$base to avoid clobbering AmigaOS startup scripts"
        continue
        ;;
      # Skip PiStorm-specific startup scripts
      *_Pistorm*|*_pistorm*|*_PISTORM*|*_Pist*|*_pist*|*_PIST*)
        echo "==> NOTE: Skipping PiStorm-specific script: $base"
        continue
        ;;
    esac
    if [[ -d "$item" ]]; then
      # Skip PiStorm-specific subdirectories
      case "$base" in
        OneTimeRun)
          # Filter OneTimeRun contents: skip PiStorm-specific scripts
          ensure_img_dir "$SYS_ROOT/S/$base"
          while IFS= read -r -d '' subitem; do
            subbase="$(basename "$subitem")"
            case "$subbase" in
              *[Pp]istorm*|*[Pp]ist*)
                echo "==> NOTE: Skipping PiStorm-specific OneTimeRun script: $subbase"
                continue
                ;;
            esac
            if [[ -d "$subitem" ]]; then
              fs_copy_tree_filtered "$subitem" "$SYS_ROOT/S/$base/$subbase"
            else
              if is_skippable_overlay_file "$subbase"; then continue; fi
              fs_copy_file "$subitem" "$SYS_ROOT/S/$base/$subbase"
            fi
          done < <(find "$item" -mindepth 1 -maxdepth 1 -print0)
          continue
          ;;
      esac
      fs_copy_tree_filtered "$item" "$SYS_ROOT/S/$base"
    else
      if is_skippable_overlay_file "$base"; then
        continue
      fi
      fs_copy_file "$item" "$SYS_ROOT/S/$base"
    fi
  done < <(find "$SYS_PAYLOAD/S" -mindepth 1 -maxdepth 1 -print0)
fi

# --- 5b) Skip PiStorm drawer — not applicable to Classic Amiga ---
# (PiStorm tools, Emu68 tools are PiStorm-specific; Classic Amiga doesn't need them)

# --- 5c) Overlay Programs drawer (optional apps; safe to add) ---
if [[ -d "$SYS_PAYLOAD/Programs" ]]; then
  echo "==> Overlay: System/Programs -> SYS:Programs"
  fs_copy_tree_filtered "$SYS_PAYLOAD/Programs" "$SYS_ROOT/Programs"
fi

# --- 5d) Overlay optional drawers ---
for _d in OneTimeRun OneTimeRunWB; do
  if [[ -d "$SYS_PAYLOAD/$_d" ]]; then
    echo "==> Overlay: System/$_d -> SYS:S/$_d"
    # Filter out PiStorm-specific scripts
    ensure_img_dir "$SYS_ROOT/S/$_d"
    while IFS= read -r -d '' subitem; do
      subbase="$(basename "$subitem")"
      case "$subbase" in
        *[Pp]istorm*|*[Pp]ist*)
          echo "==> NOTE: Skipping PiStorm-specific script: $subbase"
          continue
          ;;
      esac
      if [[ -d "$subitem" ]]; then
        fs_copy_tree_filtered "$subitem" "$SYS_ROOT/S/$_d/$subbase"
      else
        if is_skippable_overlay_file "$subbase"; then continue; fi
        fs_copy_file "$subitem" "$SYS_ROOT/S/$_d/$subbase"
      fi
    done < <(find "$SYS_PAYLOAD/$_d" -mindepth 1 -maxdepth 1 -print0)
  fi
done
for _d in Other Patch Support Locale; do
  if [[ -d "$SYS_PAYLOAD/$_d" ]]; then
    echo "==> Overlay: System/$_d -> SYS:$_d"
    fs_copy_tree_filtered "$SYS_PAYLOAD/$_d" "$SYS_ROOT/$_d"
  fi
done

# --- 5d.1) Overlay ADDITIONAL files from Assets into core drawers (C, Devs, Libs, Prefs) ---
for _core_dir in C Devs Libs Prefs; do
  if [[ -d "$SYS_PAYLOAD/$_core_dir" ]]; then
    echo "==> Overlay: Assets/System/$_core_dir -> SYS:$_core_dir (file-by-file additions)"
    ensure_img_dir "$SYS_ROOT/$_core_dir"
    while IFS= read -r -d '' item; do
      _base="$(basename "$item")"
      # Skip VideoCore-specific files in Devs (PiStorm-only RTG driver)
      if [[ "$_core_dir" == "Devs" ]]; then
        case "$_base" in
          VideoCore*|videocore*)
            echo "==> NOTE: Skipping PiStorm-specific RTG device file: $_base"
            continue
            ;;
          NetInterfaces)
            # Copy NetInterfaces but filter out PiStorm-specific interfaces
            echo "==> Overlay: Devs/NetInterfaces (filtering PiStorm interfaces)"
            ensure_img_dir "$SYS_ROOT/Devs/NetInterfaces"
            while IFS= read -r -d '' _ni; do
              _ni_base="$(basename "$_ni")"
              case "$_ni_base" in
                genet|wifipi|genet.info|wifipi.info)
                  echo "==> NOTE: Skipping PiStorm-specific interface: $_ni_base"
                  continue
                  ;;
              esac
              fs_copy_file "$_ni" "$SYS_ROOT/Devs/NetInterfaces/$_ni_base"
            done < <(find "$item" -mindepth 1 -maxdepth 1 -print0)
            continue
            ;;
        esac
      fi
      if [[ -d "$item" ]]; then
        fs_copy_tree_filtered "$item" "$SYS_ROOT/$_core_dir/$_base"
      else
        if is_skippable_overlay_file "$_base"; then
          continue
        fi
        fs_copy_file "$item" "$SYS_ROOT/$_core_dir/$_base"
      fi
    done < <(find "$SYS_PAYLOAD/$_core_dir" -mindepth 1 -maxdepth 1 -print0)
  fi
done

# Ensure core assigns (notably FONTS:) without touching Startup-Sequence.
ensure_user_startup_executes_amigaimager_assigns "$SYS_ROOT"

# --- 5e1) Install HD Startup-Sequence for WB 3.2+ ---
# Must happen BEFORE overlay injection so overlays are applied to the HD version.
# The base Workbench ADF contains Startup-Sequence_LD (low-density floppy version)
# which has a simple V47 check + Quit. The Update ADF contains
# Startup-HardDrive which has proper LoadModule support for older Kickstarts,
# ENVARC handling, and is what the official installer puts on hard drives.
# If the Update ADF's HD Startup-Sequence is available, use it.
#
# IMPORTANT: The HD Startup-Sequence contains a canonical ROM CheckInstall block
# that handles ALL Kickstart versions via runtime Version checks:
#   KS 3.1 (V40):     LoadModule L:System-Startup ROMUPDATE
#   KS 3.2.0:          LoadModule DOWNGRADE ... dos.library gadtools.library
#   KS 3.2.1/3.2.2:    LoadModule DOWNGRADE ... intuition.library Modules/bootmenu
#   KS 3.2.3+:         no-op (already current)
# This block is known working on real hardware (canonical Hyperion code).
# Do NOT sed-patch or modify it — install as-is.
if [[ "${_MODULES_INSTALLED:-0}" -eq 1 ]]; then
  _hd_ss=""
  # Search for Startup-HardDrive in Update ADFs (may be .Z compressed)
  for _upd_adf in "$ADF_SRC"/Update*.adf "$ADF_SRC"/update*.adf; do
    [[ -f "$_upd_adf" ]] || continue
    _upd_tmp="$STAGE/update-extract-ss.$$"
    rm -rf "$_upd_tmp" && mkdir -p "$_upd_tmp"
    "$HST_IMAGER" fs extract "$_upd_adf" "$_upd_tmp" --recursive --force >/dev/null 2>&1 || continue
    for _hd_cand in "$_upd_tmp"/Update/Startup-HardDrive "$_upd_tmp"/Update/Startup-HardDrive.Z; do
      if [[ -f "$_hd_cand" ]]; then
        # Decompress if needed
        if [[ "$_hd_cand" == *.Z ]]; then
          uncompress -f "$_hd_cand" 2>/dev/null || true
          _hd_cand="${_hd_cand%.Z}"
        fi
        [[ -f "$_hd_cand" ]] && { _hd_ss="$_hd_cand"; break 2; }
      fi
    done
    rm -rf "$_upd_tmp"
  done

  if [[ -n "$_hd_ss" && -f "$_hd_ss" ]]; then
    echo "==> Classic: Found HD Startup-Sequence from Update ADF"
    # Install the canonical HD Startup-Sequence unmodified — its ROM CheckInstall
    # block handles KS 3.1 through 3.2.3 automatically via Version checks.
    chmod +x "$_hd_ss"  # Amiga 's' bit: Startup-Sequence is an AmigaDOS script
    fs_copy_file "$_hd_ss" "$SYS_ROOT/S/Startup-Sequence"
    echo "==> Classic: Installed HD Startup-Sequence (replaces floppy _LD version)"
    rm -rf "$STAGE/update-extract-ss.$$" 2>/dev/null || true
  else
    echo "==> WARN: HD Startup-Sequence not found in Update ADFs; using floppy version"
  fi
fi

# --- 5e) Inject script overlays for Classic ---
# On Classic we ONLY inject:
#   - Iconlib overlay (package-specific, harmless)
#   - User-Startup package overlays (per selected package)
# We do NOT inject REXXMAST, OneTimeRun, or UAEGFX overlays because:
#   - REXXMAST: the HD Startup-Sequence already calls RexxMast. Injecting ours
#     causes a duplicate call that aborts boot with returncode 20.
#   - OneTimeRun: Startup-Sequence_OneTimeRun is PiStorm-specific — detects
#     brcm-emmc.device, sets PistormVariant, runs EMU68INFO. Useless on Classic.
#   - UAEGFX: Startup-Sequence_UAEGFX swaps Videocore/UAEGFX monitors for
#     PiStorm. Classic uses its own P96 cards; LoadMonDrvs loads the card's
#     monitor binary from DEVS:Monitors/ — no monitor swapping needed.
# The HD Startup-Sequence from Update ADF is a complete, correct boot script.
_ss="S/Startup-Sequence"
# Iconlib overlay calls C:RemLib which only exists when the RemLib Aminet
# package is installed. Only inject when RemLib is selected.
if [[ -f "$SYS_PAYLOAD/S/Startup-Sequence_Iconlib" ]] && package_is_selected "remlib"; then
  inject_overlay_into_script "$SYS_ROOT" "$_ss" \
    "$SYS_PAYLOAD/S/Startup-Sequence_Iconlib" inject_after "SetPatch" "Iconlib"
fi

# User-Startup overlays (package-specific)
_us="S/user-startup"
if ! fs_dir "$SYS_ROOT/S/user-startup"; then
  if fs_dir "$SYS_ROOT/S/User-Startup"; then
    _us="S/User-Startup"
  fi
fi

# CardPatch overlay — injected first so subsequent package overlays
# (notably Roadshow) append AFTER it in User-Startup. card.resource must
# be patched before any PCMCIA driver opens it. Auto-applied, not a
# user-selectable package — gated only by A1200 + PCMCIA NIC at build.
if [[ "${CLASSIC_MACHINE:-}" == "A1200" && "${NETWORK_CARD:-}" == PCMCIA* ]]; then
  _cp_overlay="$SYS_PAYLOAD/S/User-Startup_CardPatch"
  if [[ -f "$_cp_overlay" ]]; then
    inject_overlay_into_script "$SYS_ROOT" "$_us" "$_cp_overlay" append "" "CardPatch"
  fi
fi

for _us_overlay in "$SYS_PAYLOAD"/S/User-Startup_*; do
  [[ -f "$_us_overlay" ]] || continue
  _us_base="$(basename "$_us_overlay")"
  _us_pkg="${_us_base#User-Startup_}"
  # CardPatch is auto-applied above (A1200+PCMCIA gate), not a selectable package.
  if [[ "$_us_pkg" == "CardPatch" ]]; then
    continue
  fi
  if package_is_selected "$_us_pkg"; then
    inject_overlay_into_script "$SYS_ROOT" "$_us" "$_us_overlay" append "" "$_us_pkg"
  else
    echo "==> Skipping overlay $_us_base (package $_us_pkg not selected)"
  fi
done

# --- 5f) Binary prefs patching ---
# RTG screen mode patching handled in Classic RTG section below (if RTG selected).
# Write WBConfig prefs (backdrop setting).
write_wbconfig_prefs "$SYS_ROOT" "$BACKDROP_ENABLED"
if [[ "$BACKDROP_ENABLED" == "1" && "$_RTG_BACKDROP_PNG_INSTALLED" == "1" ]]; then
  write_wbpattern_backdrop "$SYS_ROOT" "Sys:Prefs/Presets/Backdrops/backdrop.png"
fi

# Set keyboard layout based on selected locale
if [[ -n "$LOCALES" ]]; then
  _primary_keymap=""
  IFS=',' read -ra _locale_arr <<< "$LOCALES"
  for _loc in "${_locale_arr[@]}"; do
    _km="$(locale_to_keymap "$_loc")"
    if [[ -n "$_km" ]]; then
      _primary_keymap="$_km"
      break
    fi
  done
  if [[ -n "$_primary_keymap" ]]; then
    write_keymap_prefs "$SYS_ROOT" "$_primary_keymap"
  fi
fi

# --- 5g) Disk icons ---
install_disk_icons "$SYS_ROOT"
ensure_drawer_visible_in_root "$SYS_ROOT" "Programs"

# Ensure essential C/ commands exist (parallel ADF extraction can lose files).
# AddDataTypes is needed by the HD Startup-Sequence.
for _essential_cmd in AddDataTypes; do
  if ! fs_dir "$SYS_ROOT/C/$_essential_cmd"; then
    echo "==> WARN: C/$_essential_cmd missing — recovering from Workbench ADF"
    _wb_rescue="$STAGE/wb-rescue-$$"
    rm -rf "$_wb_rescue" && mkdir -p "$_wb_rescue"
    for _wb_adf in "$ADF_SRC"/Workbench*.adf "$ADF_SRC"/workbench*.adf; do
      [[ -f "$_wb_adf" ]] || continue
      "$HST_IMAGER" fs extract "$_wb_adf" "$_wb_rescue" --recursive --force >/dev/null 2>&1 || continue
      if [[ -f "$_wb_rescue/C/$_essential_cmd" ]]; then
        fs_copy_file "$_wb_rescue/C/$_essential_cmd" "$SYS_ROOT/C/$_essential_cmd"
        echo "==> Recovered C/$_essential_cmd from $(basename "$_wb_adf")"
        break
      fi
    done
    rm -rf "$_wb_rescue"
  fi
done

# Ensure HDToolBox exists so the stock icon opens without requester.
ensure_hdtoolbox_compat "$SYS_ROOT"

# CLICon / CLIcon fallback
download_and_install_clicon "$SYS_ROOT"
if ! fs_dir "$SYS_ROOT/System/CLICon" \
   && ! fs_dir "$SYS_ROOT/System/CLIcon"; then
  if fs_dir "$SYS_ROOT/System/CLI"; then
    local _cli_tmp="$STAGE/.cli-copy-$$.$RANDOM"
    if fs_pull_file "$SYS_ROOT/System/CLI" "$_cli_tmp" 2>/dev/null; then
      fs_copy_file "$_cli_tmp" "$SYS_ROOT/System/CLIcon" 2>/dev/null || true
    fi; rm -f "$_cli_tmp" 2>/dev/null || true
    echo "==> Copied System/CLI -> System/CLIcon (WB3.2 LD fallback)"
  fi
fi

# mecho (enhanced echo for progress animations)
download_and_install_mecho "$SYS_ROOT"

# screennotify.library (for WBDock2 if installed)
download_and_install_screennotify "$SYS_ROOT"

# codesets.library — required by LumiFTP and LumiWeather for filename encoding.
if package_is_selected "lumiftp" || package_is_selected "lumiweather"; then
  download_and_install_codesets_library "$SYS_ROOT"
fi

# icon.library 46.4 (if selected)
if package_is_selected "IconLib"; then
  if ! fs_dir "$SYS_ROOT/Libs/icon.library"; then
    download_and_install_iconlib "$SYS_ROOT"
  fi
fi

# AHI prefs binary alias (AHI -> AHI_BGUI or AHI_MUI)
_ahi_prefs_src=""
if fs_dir "$SYS_ROOT/Libs/muimaster.library"; then
  _ahi_prefs_src="AHI_MUI"
else
  _ahi_prefs_src="AHI_BGUI"
  if ! fs_dir "$SYS_ROOT/Libs/bgui.library"; then
    if fs_dir "$SYS_ROOT/Programs/AHI/BGUI/bgui.library"; then
      _bgui_tmp="$STAGE/bgui.library.$$"
      if fs_pull_file "$SYS_ROOT/Programs/AHI/BGUI/bgui.library" "$_bgui_tmp" >/dev/null 2>&1; then
        fs_copy_file "$_bgui_tmp" "$SYS_ROOT/Libs/bgui.library"
        echo "==> Installed bgui.library to LIBS: (for AHI_BGUI)"
        rm -f "$_bgui_tmp" 2>/dev/null || true
      fi
    fi
  fi
fi
if fs_dir "$SYS_ROOT/Prefs/$_ahi_prefs_src"; then
  _ahi_tmp="$STAGE/ahi-prefs-bin.$$"
  if fs_pull_file "$SYS_ROOT/Prefs/$_ahi_prefs_src" "$_ahi_tmp" >/dev/null 2>&1; then
    fs_copy_file "$_ahi_tmp" "$SYS_ROOT/Prefs/AHI"
    echo "==> Copied Prefs/$_ahi_prefs_src -> Prefs/AHI"
    fs_copy_file "$_ahi_tmp" "$SYS_ROOT/Prefs/AHI.MWB"
    echo "==> Copied Prefs/$_ahi_prefs_src -> Prefs/AHI.MWB"
    rm -f "$_ahi_tmp" 2>/dev/null || true
  fi
fi

# --- 5h) Tooltypes and icon positions from Windows CSVs ---
apply_tooltypes_from_package_csv
if [[ "$APPLY_ICON_POSITIONS" == "1" ]]; then
  apply_icon_positions_from_csv
else
  echo "==> Icon positioning: skipped (disabled)"
fi

# --- 5i) Set post-overlay icons borderless ---
set_coloricons_borderless_image "$SYS_ROOT" "Tools" "System" "Prefs" "Programs" "Utilities" "WBStartup" "Devs" "Storage"

# --- post-overlay sanity checks ---
if ! fs_dir "$SYS_ROOT/System/CLI"; then
  echo "==> WARN: SYS:System/CLI missing (Shell icon will fail)." >&2
fi
if ! fs_dir "$SYS_ROOT/Fonts"; then
  echo "==> WARN: SYS:Fonts missing (Font prefs will fail)." >&2
fi
if ! fs_dir "$SYS_ROOT/Tools"; then
  echo "==> WARN: SYS:Tools missing (Calculator/ShowConfig etc will fail)." >&2
fi

# Persist a tiny note for debugging on the Amiga side
TMP_ADF_NOTE="$STAGE/amigaimager-adf-staged.txt"
if [[ "$IS_39_ISO" -eq 1 ]]; then
  printf "source:OS3.9-CD-ISO\nplatform:Classic-Amiga\n" > "$TMP_ADF_NOTE"
else
  printf "adf-staged-from:%s\nplatform:Classic-Amiga\n" "$ADF_SRC" > "$TMP_ADF_NOTE"
fi
make_lf_only "$TMP_ADF_NOTE"
fs_copy_file "$TMP_ADF_NOTE" "$SYS_ROOT/Install/amigaimager-adf-staged.txt"

# -----------------------------------------------------------------------------
# 6) Mandatory LhA archive helper
# -----------------------------------------------------------------------------
download_lha_from_aminet() {
  local urls=(
    "https://aminet.net/util/arc/lha_68040.lha"
    "https://aminet.net/util/arc/lha_68020.lha"
    "https://aminet.net/util/arc/lha_68k.lha"
    "http://de.aminet.net/pub/aminet/util/arc/lha_68040.lha"
    "http://de.aminet.net/pub/aminet/util/arc/lha_68020.lha"
    "http://de.aminet.net/pub/aminet/util/arc/lha_68k.lha"
  )
  local archive=""
  for url in "${urls[@]}"; do
    local base
    base="$(basename "$url")"
    archive="$STAGE/$base"
    echo "==> Downloading mandatory LhA package: $base" >&2
    if curl -fL --connect-timeout 5 --max-time 20 --retry 1 --retry-delay 1 -o "$archive" "$url" >/dev/null 2>&1; then
      echo "$archive"
      return 0
    fi
  done
  return 1
}

# -----------------------------------------------------------------------------
# 7) Optional Transfer folder -> SYS:Transfer/
# -----------------------------------------------------------------------------
if [[ -n "$TRANSFER_DIR" ]]; then
  [[ -d "$TRANSFER_DIR" ]] || die "Transfer dir missing: $TRANSFER_DIR"
  fs_copy "$TRANSFER_DIR/" "$TRANSFER_ROOT/Transfer/" --recursive --makedir
  ensure_transfer_drawer_icon "$TRANSFER_ROOT"
fi

# -----------------------------------------------------------------------------
# 8) Stage OneTimeRunWB payload
# -----------------------------------------------------------------------------
echo "==> Stage OneTimeRunWB payload (if available in Assets)"

TMP_STAGE_MARK="$STAGE/amigaimager-staged.txt"
printf "staged:%s\nplatform:classic\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$TMP_STAGE_MARK"
make_lf_only "$TMP_STAGE_MARK"

fs_mkdir_p "$SYS_ROOT/S"
fs_mkdir_p "$SYS_ROOT/S/OneTimeRunWB"

if ! fs_dir "$SYS_ROOT/S"; then
  die "Failed to create/access SYS:S at $SYS_ROOT/S"
fi
if ! fs_dir "$SYS_ROOT/S/OneTimeRunWB"; then
  die "Failed to create/access SYS:S/OneTimeRunWB at $SYS_ROOT/S/OneTimeRunWB"
fi

fs_copy_file "$TMP_STAGE_MARK" "$SYS_ROOT/S/amigaimager-staged.txt"

CAND_SYS_OTRWB="$ASSETS_DIR/AmigaFiles/System/OneTimeRunWB"
CAND1="$ASSETS_DIR/AmigaFiles/OneTimeRunWB"
CAND2="$ASSETS_DIR/AmigaFiles/System/S/OneTimeRunWB"

ONETIMERUNWB_SRC=""
if [[ -d "$CAND_SYS_OTRWB" ]]; then
  ONETIMERUNWB_SRC="$CAND_SYS_OTRWB"
elif [[ -d "$CAND1" ]]; then
  ONETIMERUNWB_SRC="$CAND1"
elif [[ -d "$CAND2" ]]; then
  ONETIMERUNWB_SRC="$CAND2"
fi

if [[ -n "$ONETIMERUNWB_SRC" ]]; then
  stage_onetimerunwb_payload_filtered "$ONETIMERUNWB_SRC" "$SYS_ROOT/S/OneTimeRunWB"
else
  echo "==> NOTE: No OneTimeRunWB payload found in Assets."
fi

# localesettings: ensure Locale.prefs exists
if fs_dir "$SYS_ROOT/Prefs/Env-Archive/Sys"; then
  # Copy (best-effort) using pull+push for dispatch compatibility.
  _lp_tmp="$STAGE/.locale-prefs-$$.$RANDOM"
  if fs_pull_file "$SYS_ROOT/Prefs/Presets/Locale.prefs" "$_lp_tmp" 2>/dev/null; then
    fs_copy_file "$_lp_tmp" "$SYS_ROOT/Prefs/Env-Archive/Sys/Locale.prefs" 2>/dev/null || true
  fi; rm -f "$_lp_tmp" 2>/dev/null || true
  _lpi_tmp="$STAGE/.locale-prefs-info-$$.$RANDOM"
  if fs_pull_file "$SYS_ROOT/Prefs/Presets/Locale.prefs.info" "$_lpi_tmp" 2>/dev/null; then
    fs_copy_file "$_lpi_tmp" "$SYS_ROOT/Prefs/Env-Archive/Sys/Locale.prefs.info" 2>/dev/null || true
  fi; rm -f "$_lpi_tmp" 2>/dev/null || true
fi

# -----------------------------------------------------------------------------
# 8a) Stage optional packages
# -----------------------------------------------------------------------------
echo "==> Stage optional packages"
PKG_ROOT="$ASSETS_DIR/AmigaFiles/Packages"

if [[ -n "$PACKAGES" ]]; then
  IFS=',' read -r -a PKG_LIST <<< "$PACKAGES"
  for pkg in "${PKG_LIST[@]}"; do
    pkg="$(echo "$pkg" | tr -d ' \t\n\r')"
    [[ -z "$pkg" ]] && continue

    SRC="$PKG_ROOT/$pkg"
    DST="$SYS_ROOT/S/OneTimeRunWB/$pkg"

    if [[ -d "$SRC" ]]; then
      echo "  -> staging package: $pkg"
      stage_onetimerunwb_payload_filtered "$SRC" "$DST"
    fi
    # Non-existing IDs are Aminet/external packages handled elsewhere — silently skip.
  done

  TMP_MANIFEST="$STAGE/amigaimager-packages.txt"
  printf "%s\n" "$PACKAGES" > "$TMP_MANIFEST"
  make_lf_only "$TMP_MANIFEST"
  fs_copy "$TMP_MANIFEST" "$SYS_ROOT/Install/amigaimager-packages.txt" --makedir || true
else
  echo "==> No packages selected"
fi

# -----------------------------------------------------------------------------
# 8b) Stage Aminet packages - build-time installation
# -----------------------------------------------------------------------------
if [[ -n "$AMINET_DIR" && -d "$AMINET_DIR" ]]; then
  echo "==> Install Aminet packages from: $AMINET_DIR"
  AMINET_RULES_USED="$(resolve_aminet_rules_csv || true)"
  if [[ -n "$AMINET_RULES_USED" ]]; then
    echo "==> Aminet mapping model: CSV ($(basename "$AMINET_RULES_USED"))"
  else
    echo "==> Aminet mapping model: legacy fallback (CSV rules not found)"
  fi

  declare -a AMINET_BUILDTIME=()
  declare -a AMINET_UNMAPPED=()
  declare -a AMINET_FILES=()
  LHA_READY=0

  for lha_file in "$AMINET_DIR"/*.lha "$AMINET_DIR"/*.LHA; do
    [[ -f "$lha_file" ]] || continue

    # Skip PiStorm-specific Aminet packages (VideoCore is PiStorm-only)
    lha_base_check="$(basename "$lha_file")"
    case "$(printf '%s' "$lha_base_check" | tr '[:upper:]' '[:lower:]')" in
      videocore*.lha)
        echo "  -> skipping PiStorm-specific Aminet package: $lha_base_check"
        continue
        ;;
    esac

    # IBrowse/AmiSSL/WBDock have dedicated installers; skip them here.
    is_dedicated_external_archive "$lha_base_check" && continue

    AMINET_FILES+=("$lha_file")
    lha_base="$(basename "$lha_file")"
    case "$(printf '%s' "$lha_base" | tr '[:upper:]' '[:lower:]')" in
      lha_68040.lha|lha_68020.lha|lha_68k.lha) LHA_READY=1 ;;
    esac
  done

  # WHDLoad is sourced from the external staging dir (whdload.de) — add it to AMINET_FILES
  # so the CSV install loop processes it alongside the Aminet packages.
  # Skipped when --packages-dir merged the staging dirs (the glob above already has it).
  if [[ -n "${IBROWSE_DIR:-}" && -d "${IBROWSE_DIR:-}" && "${IBROWSE_DIR}" != "${AMINET_DIR:-}" ]]; then
    for _whdl in "$IBROWSE_DIR"/WHDLoad_usr.lha "$IBROWSE_DIR"/WHDLoad*.lha; do
      [[ -f "$_whdl" ]] || continue
      AMINET_FILES+=("$_whdl")
      echo "==> WHDLoad: adding $(basename "$_whdl") from external staging dir"
      break
    done
  fi

  # LhA is mandatory
  if [[ "${LHA_READY:-0}" -ne 1 ]]; then
    lha_archive="$(download_lha_from_aminet || true)"
    if [[ -n "${lha_archive:-}" && -f "$lha_archive" ]]; then
      echo "==> Aminet: using downloaded mandatory package $(basename "$lha_archive")"
      AMINET_FILES+=("$lha_archive")
      LHA_READY=1
    else
      echo "==> WARN: Aminet: mandatory LhA archive was not found locally and could not be downloaded."
    fi
  fi

  for lha_file in "${AMINET_FILES[@]}"; do
    lha_base="$(basename "$lha_file")"

    # Skip Aminet picasso96.lha if iComp P96 archive is provided
    if [[ -n "${P96_ICOMP_ARCHIVE:-}" ]] && echo "$lha_base" | grep -qi "^picasso96\.lha$"; then
      echo "  -> skipping Aminet $lha_base (iComp P96 archive provided)"
      continue
    fi

    # mpega.library — CPU-aware install. Determine class once and reuse.
    if echo "$lha_base" | grep -qi "^mpega_library\.lha$"; then
      _mpega_cpu_class="000"
      if [[ "$(classic_is_68040plus)" == "1" ]]; then
        _mpega_cpu_class="fpu"
      elif [[ "$(classic_is_68020plus)" == "1" ]]; then
        _mpega_cpu_class="020"
      fi
      echo "  -> processing Aminet: $lha_base (cpu_class=${_mpega_cpu_class})"
      pkg_time_start
      install_mpega_library "$SYS_ROOT" "$lha_file" "$_mpega_cpu_class"
      pkg_time_done "install_mpega_library"
      AMINET_BUILDTIME+=("$lha_base")
      continue
    fi

    # RiVA video player (m68k binary) requires 68040+ — skip on 68000/020/030.
    if echo "$lha_base" | grep -qi "^riva-.*\.lha$" && [[ "$(classic_is_68040plus)" != "1" ]]; then
      echo "  -> skipping $lha_base (requires 68040+; target CPU is below 68040)"
      continue
    fi

    # ScummVM — 68060-only; illegal instruction on 68020/030/040 (Phase 6: wrapper)
    if is_scummvm_archive "$lha_base"; then
      if [[ "${ACCELERATOR:-}" != "68060" ]]; then
        echo "  -> skipping $lha_base (ScummVM 060 build requires 68060; target is ${ACCELERATOR:-stock})"
      else
        install_pkg_scummvm "$SYS_ROOT" "$lha_file" "$lha_base" "060" && AMINET_BUILDTIME+=("$lha_base")
      fi
      continue
    fi

    # AmigaAMP — explicit installer handles the AmigaAMP3 archive layout directly.
    if is_amigaamp_archive "$lha_base"; then
      if install_pkg_amigaamp "$SYS_ROOT" "$lha_file" "$lha_base"; then
        AMINET_BUILDTIME+=("$lha_base")
        continue
      fi
      echo "  -> WARN: explicit AmigaAMP install failed, falling back to CSV mapping"
    fi

    # rc-ftpd — explicit installer (multi-destination: Programs/, Libs/, ENVARC/).
    if is_rcftpd_archive "$lha_base"; then
      if install_pkg_rcftpd "$SYS_ROOT" "$lha_file" "$lha_base"; then
        AMINET_BUILDTIME+=("$lha_base")
        continue
      fi
      echo "  -> WARN: explicit rc-ftpd install failed, falling back to CSV mapping"
    fi

    # GoVD — explicit installer + optional key file injection.
    if is_govd_archive "$lha_base"; then
      if install_pkg_govd "$SYS_ROOT" "$lha_file" "$lha_base" "${GOVD_KEY:-}"; then
        AMINET_BUILDTIME+=("$lha_base")
        continue
      fi
      echo "  -> WARN: explicit GoVD install failed, falling back to CSV mapping"
    fi

    # IconLib_46.4's TC020 variant (icon.library 51.4.602) requires a 68020+ CPU.
    # On stock 68000 machines (A500, A600, A2000 without accelerator), the 68020
    # instructions cause illegal instruction exceptions → empty Workbench.
    # There is no compatible PeterK icon.library for 68000 + OS 3.2 (V47 requires
    # icon.library >= V47, but the 68000 variant is stuck at V46).
    # Skip IconLib entirely on stock 68000 systems.
    _cpu_type_for_iconlib="${_cpu_type:-none}"
    if [[ "$_cpu_type_for_iconlib" == "none" ]]; then
      # Stock CPU — determine from machine type
      case "${CLASSIC_MACHINE:-}" in
        A500|A600|A2000) _cpu_type_for_iconlib="68000" ;;
        *)               _cpu_type_for_iconlib="68020" ;;  # A1200/A3000/A4000 have 020+
      esac
    fi
    if [[ "$_cpu_type_for_iconlib" == "68000" ]] && echo "$lha_base" | grep -qi "^IconLib_"; then
      echo "  -> skipping Aminet $lha_base (TC020 requires 68020+, stock 68000 machine)"
      continue
    fi
    # Roadie (Roadshow GUI helper) requires 68020+ — skip on stock 68000
    if [[ "$_cpu_type_for_iconlib" == "68000" ]] && echo "$lha_base" | grep -qi "^Roadie"; then
      echo "  -> skipping Aminet $lha_base (requires 68020+, stock 68000 machine)"
      continue
    fi

    # Solas, LumiWeather, LumiPass, LumiReg — simple archive extractors.
    _simple_app_done=0
    for _fn in solas lumiweather lumipass lumireg; do
      _fn_pred="is_${_fn}_archive"
      _fn_inst="install_pkg_${_fn}"
      if "$_fn_pred" "$lha_base"; then
        if "$_fn_inst" "$SYS_ROOT" "$lha_file" "$lha_base"; then
          AMINET_BUILDTIME+=("$lha_base")
          _simple_app_done=1
          break
        fi
        echo "  -> WARN: explicit ${_fn} install failed, falling back to CSV mapping"
      fi
    done
    # continue the outer package loop — without this the archive was re-fed
    # to the CSV path and misreported as "no usable CSV/file match"
    if [[ "$_simple_app_done" -eq 1 ]]; then
      continue
    fi

    # User-defined packages (Settings → custom download URL): plan-driven generic install
    if install_pkg_user_archive "$SYS_ROOT" "$lha_file" "$lha_base"; then
      AMINET_BUILDTIME+=("$lha_base")
      continue
    fi

    echo "  -> processing Aminet: $lha_base"
    if install_aminet_package_buildtime "$SYS_ROOT" "$lha_file"; then
      AMINET_BUILDTIME+=("$lha_base")
      case "$(printf '%s' "$lha_base" | tr '[:upper:]' '[:lower:]')" in
        lha_68040.lha|lha_68020.lha|lha_68k.lha) LHA_READY=1 ;;
      esac
    else
      AMINET_UNMAPPED+=("$lha_base")
    fi
  done

  echo "==> Aminet: ${#AMINET_BUILDTIME[@]} packages installed at build-time"

  if [[ "${#AMINET_UNMAPPED[@]}" -gt 0 ]]; then
    echo "==> Aminet: ${#AMINET_UNMAPPED[@]} packages had no usable CSV/file match and were skipped:"
    for lha_base in "${AMINET_UNMAPPED[@]}"; do
      echo "    - $lha_base"
    done
  fi

  if img_dir_has_entry_ci "$SYS_ROOT/C" "lha"; then
    LHA_READY=1
  else
    LHA_READY=0
  fi

  if [[ "${LHA_READY:-0}" -ne 1 ]]; then
    die "Mandatory package LhA missing in SYS:C. Add lha_68040.lha (or lha_68020.lha/lha_68k.lha) to --aminet-dir."
  fi
else
  echo "==> No Aminet packages to stage"
fi
normalize_roadie_install "$SYS_ROOT"
fixup_ahi_prefs "$SYS_ROOT"

# --- Post-Aminet package fixups ---
finalize_programs_entries "$SYS_ROOT"

# ScummVM freeware games — strategy-dispatched loop (Phase 3)
for _game in bass fotaq lure; do
  run_scummvm_game_pkg "$_game" "$SYS_ROOT"
done
apply_scummvm_game_entries "$SYS_ROOT"

# DOpus416: enable FORCEOPENXAD ToolType
for lha_base in "${AMINET_BUILDTIME[@]+"${AMINET_BUILDTIME[@]}"}"; do
  case "$(printf '%s' "$lha_base" | tr '[:upper:]' '[:lower:]')" in
    dopus414jrbin.lha|dopus416jrbin.lha)
      apply_dopus_tooltype_fix "$SYS_ROOT"
      ;;
  esac
done

# WHDLoadWrapper: install if WHDLoad was staged
for lha_base in "${AMINET_BUILDTIME[@]+"${AMINET_BUILDTIME[@]}"}"; do
  case "$(printf '%s' "$lha_base" | tr '[:upper:]' '[:lower:]')" in
    whdload_usr.lha|whdload-user.lha|whdload.lha)
      download_and_install_whdload_wrapper "$SYS_ROOT"
      ;;
  esac
done

# WHDLoad kickstart ROMs
if [[ -n "${WHDLOAD_KICKSTARTS_DIR:-}" && -d "${WHDLOAD_KICKSTARTS_DIR:-}" ]]; then
  install_whdload_kickstarts "$SYS_ROOT" "$WHDLOAD_KICKSTARTS_DIR"
fi

# IMP3 registration: file takes precedence over manual code
if [[ -n "${IMP3_REG_FILE:-}" && -f "$IMP3_REG_FILE" ]]; then
  if fs_dir "$SYS_ROOT/Programs/IMP3" \
     || fs_dir "$SYS_ROOT/C/IMP3"; then
    fs_copy_file "$IMP3_REG_FILE" "$SYS_ROOT/S/imp3.reg"
    echo "==> IMP3: registration file copied to S:imp3.reg"
  else
    echo "==> IMP3: reg file provided but IMP3 not found; skipping"
  fi
elif [[ -n "${IMP3_REG:-}" ]]; then
  if fs_dir "$SYS_ROOT/Programs/IMP3" \
     || fs_dir "$SYS_ROOT/C/IMP3"; then
    local_imp3_reg="$STAGE/imp3.reg"
    printf '%s' "$IMP3_REG" > "$local_imp3_reg"
    fs_copy_file "$local_imp3_reg" "$SYS_ROOT/S/imp3.reg"
    echo "==> IMP3: registration code written to S:imp3.reg"
  else
    echo "==> IMP3: reg code provided but IMP3 not found; skipping"
  fi
fi

# -----------------------------------------------------------------------------
# Classic Hardware: Accelerator CPU library
# -----------------------------------------------------------------------------
if [[ -n "$ACCELERATOR" && "$ACCELERATOR" != "none" ]]; then
  echo "==> Classic: Accelerator = $ACCELERATOR"
  # Map accelerator card to CPU type
  _cpu_type=""
  _is_aca_card=0
  case "$ACCELERATOR" in
    68030)                              _cpu_type="68030" ;;
    68040)                              _cpu_type="68040" ;;
    68060)                              _cpu_type="68060" ;;
    ACA1221|ACA1232|ACA1233n|ACA1234)   _cpu_type="68030"; _is_aca_card=1 ;;
    *) echo "==> WARN: Unknown accelerator: $ACCELERATOR" ;;
  esac
  _cpu_lib=""
  case "$_cpu_type" in
    68040) _cpu_lib="68040.library" ;;
    68060) _cpu_lib="68060.library" ;;
    68030) echo "==> Classic: 68030 (no extra CPU library needed)" ;;
  esac
  if [[ -n "$_cpu_lib" ]]; then
    _tmp_check="$STAGE/.check-cpulib-$$"
    if fs_pull_file "$SYS_ROOT/Libs/$_cpu_lib" "$_tmp_check" >/dev/null 2>&1; then
      echo "==> Classic: $_cpu_lib found in SYS:Libs (from ADFs)"
      rm -f "$_tmp_check"
    else
      echo "==> WARN: $_cpu_lib NOT found in SYS:Libs — your $ACCELERATOR accelerator may not work correctly." >&2
      echo "==>   This library is normally included in AmigaOS 3.1.4+ and 3.2 install disks." >&2
    fi
  fi

  # Install MMU library and tools for 68030+ accelerators.
  # MMULibs.adf extracts to SYS_HOST during ADF merge; verify key files.
  _mmu_ok=0
  _mmu_check="$STAGE/.check-mmulib-$$"
  _skip_mmu_overlay=0
  if [[ "$RTG_CARD" == "VampireSAGA" ]]; then
    _skip_mmu_overlay=1
    echo "==> Classic: MMULibs intentionally omitted for Vampire SAGA (Apollo 68080 is not a standard 68040 MMU target)"
    echo "==> Classic: Skipping MMULibs fast-memory overlay for Vampire SAGA (Apollo 68080 is not a standard 68040 MMU target)"
  elif fs_pull_file "$SYS_ROOT/Libs/mmu.library" "$_mmu_check" >/dev/null 2>&1; then
    _mmu_ok=1
    echo "==> Classic: mmu.library found in SYS:Libs (from MMULibs ADF)"
    rm -f "$_mmu_check"
  else
    echo "==> WARN: mmu.library NOT found in SYS:Libs — MMULibs.adf may be missing from ADF set." >&2
  fi

  # Inject MMU overlay into User-Startup on the image.
  # MuFastRom maps Kickstart ROM to 32-bit Fast RAM for accelerated execution.
  # MuFastChip mirrors Chip RAM to Fast RAM for faster CPU access.
  #
  # Skip on iComp ACA cards: ACATool activates the ACA's HARDWARE MapROM
  # (see install_acatool_for_classic). Running MuFastRom on top of that
  # has the CPU MMU and the ACA hardware fight over ROM shadow mapping,
  # which corrupts the MMU state on the second boot pass and grey-screens
  # the machine. ACA cards own ROM mapping entirely — MuFastRom must not
  # run alongside ACATool. Vampire boards also skip this path because the
  # Apollo 68080 is not a drop-in MMU target for generic 68040 MMULibs setup.
  if [[ "$_mmu_ok" -eq 1 && "$_is_aca_card" -eq 0 && "$_skip_mmu_overlay" -eq 0 ]]; then
    _mmu_overlay="$STAGE/.mmu-overlay-$$"
    cat > "$_mmu_overlay" <<'MMUEOF'
; MMU Library configuration — Amiga Imager
; Maps Kickstart ROM to 32-bit Fast RAM for accelerated execution.
IF EXISTS C:MuFastRom
  C:MuFastRom
ENDIF
IF EXISTS C:MuFastChip
  C:MuFastChip
ENDIF
MMUEOF

    # Determine User-Startup path on image (case varies)
    _us_path="S/User-Startup"
    if ! fs_dir "$SYS_ROOT/$_us_path"; then
      fs_dir "$SYS_ROOT/S/user-startup" && _us_path="S/user-startup"
    fi
    inject_overlay_into_script "$SYS_ROOT" "$_us_path" "$_mmu_overlay" append "" "MMULibs"
    rm -f "$_mmu_overlay"
    echo "==> Classic: Injected MMU overlay into User-Startup (MuFastRom + MuFastChip)"
  fi

  # iComp ACA cards: install ACATool and activate MapROM with the user's
  # Kickstart ROM file. This loads V47 Kickstart into the card's MapROM
  # area at boot, which survives warm reset (unlike LoadModule on SDRAM-only cards).
  if [[ "$_is_aca_card" -eq 1 && -f "$KICK_ROM" ]]; then
    echo "==> Classic: Installing ACATool for iComp $ACCELERATOR MapROM activation"
    install_acatool_for_classic "$SYS_ROOT" "$KICK_ROM" "$ACCELERATOR"
  fi
fi

echo "==> PROGRESS 6/10 Configuring RTG and network"
# -----------------------------------------------------------------------------
# Classic RTG: Stage P96 if an RTG card was selected
# -----------------------------------------------------------------------------
if [[ -n "$RTG_CARD" && "$RTG_CARD" != "none" ]]; then
  echo "==> Classic RTG: Card = $RTG_CARD (driver: ${RTG_CARD_DRIVER:-auto})"
  if [[ "$RTG_CARD" == "PVision" ]]; then
    case "${CLASSIC_MACHINE:-}" in
      A600|A1200) ;;
      *) die "P-Vision is only supported on Classic A600/A1200 builds (selected: ${CLASSIC_MACHINE:-unknown})" ;;
    esac
    [[ -n "${P96_ICOMP_ARCHIVE:-}" ]] || die "P-Vision requires a Picasso96 iComp archive (3.6.3+). Aminet shareware fallback is disabled for this card."
  fi

  # Classic doesn't use PiStorm Videocore/Uaegfx monitor naming — install the
  # Picasso96 monitor under its original name so P96 cards can find it.
  export SKIP_VIDEOCORE_MONITOR=1
  # Real Classic P96 cards (ZZ9000, Picasso IV, CyberVision, etc.) need the
  # iComp v43+ rtg.library ABI. Don't force v40 shareware replacement.
  export P96_TRUST_ARCHIVE_ABI=1
  export P96_INSTALL_GENERIC_MONITOR=1
  stage_p96_icomp_archive "$SYS_ROOT"

  # Install the Picasso96 generic monitor for Classic cards that use the generic name.
  # VA2000, ZZ9000, P-Vision, and VampireSAGA install their own NAMED monitors instead.
  # Leaving Devs/Monitors/Picasso96 in the image alongside VA2000.card/ZZ9000.card
  # causes P96 to report "Could not create graphics board context for 'Picasso96'"
  # because no Picasso96.card exists to match the generic monitor name.
  _p96_ext="$STAGE/p96-extract"
  if [[ -d "$_p96_ext" && "$RTG_CARD" != "VA2000" && "$RTG_CARD" != "ZZ9000" && "$RTG_CARD" != "PVision" && "$RTG_CARD" != "VampireSAGA" ]]; then
    _p96_mon="$(find "$_p96_ext" -type f -iname 'Picasso96' -path '*/monitors/*' 2>/dev/null | head -1)"
    if [[ -n "$_p96_mon" && -f "$_p96_mon" ]]; then
      ensure_img_dir "$SYS_ROOT/Devs/Monitors"
      fs_copy_file "$_p96_mon" "$SYS_ROOT/Devs/Monitors/Picasso96"
      echo "==> Classic RTG: installed Devs/Monitors/Picasso96"
      _p96_mon_info="${_p96_mon}.info"
      if [[ -f "$_p96_mon_info" ]]; then
        fs_copy_file "$_p96_mon_info" "$SYS_ROOT/Devs/Monitors/Picasso96.info"
      fi
    fi
  fi

  # Card-specific driver downloads: VA2000, Vampire SAGA, ZZ9000
  if [[ "$RTG_CARD" == "VA2000" ]]; then
    install_va2000_for_classic "$SYS_ROOT"
  fi
  if [[ "$RTG_CARD" == "PVision" ]]; then
    install_pvision_for_classic "$SYS_ROOT" || die "P-Vision installation failed"
  fi
  if [[ "$RTG_CARD" == "VampireSAGA" ]]; then
    echo "==> WARN: Vampire SAGA RTG is EXPERIMENTAL — behaviour may differ from standard 68040"
    install_vampiresaga_for_classic "$SYS_ROOT"
  fi
  if [[ "$RTG_CARD" == "ZZ9000" ]]; then
    install_zz9000_for_classic "$SYS_ROOT"
  fi

  # P96 ModeIDs use resolution-based $50xx prefixes that are universal across
  # all P96 cards (CyberVision, Picasso IV, UAEGFX, etc.). Write screenmode.prefs
  # with the user's selected resolution so the RTG mode is pre-configured.
  write_screenmode_prefs "$SYS_ROOT" "$SCREEN_MODE_ID" "$WB_DEPTH_HEX"

  # Inject a ScreenMode call just before IPrefs in the Startup-Sequence.
  # The WB 3.2 HD Startup-Sequence order is:
  #   BindDrivers → LoadMonDrvs (loads DEVS:Monitors/ binaries) → IPrefs → LoadWB
  # By the time our overlay runs (just before IPrefs), LoadMonDrvs has already
  # loaded the P96 monitor binary (e.g. ZZ9000, vampiregfx), so RTG modes are
  # registered. Apply the active screenmode.prefs here so user-saved ScreenMode
  # changes persist across reboots instead of being overwritten by the build-time
  # seed in screenmode.prefs.User.
  _classic_sm_ol="$STAGE/Startup-Sequence_ClassicRTGScreenMode"
  cat > "$_classic_sm_ol" <<'__CLASSIC_SM__'
IF EXISTS SYS:Prefs/Env-Archive/Sys/screenmode.prefs
  IF $ScreenModeChipset EQ "RTG"
    Sys:Prefs/ScreenMode FROM SYS:Prefs/Env-Archive/Sys/screenmode.prefs USE >NIL:
  ENDIF
ENDIF
__CLASSIC_SM__
  make_lf_only "$_classic_sm_ol"
  inject_overlay_into_script "$SYS_ROOT" "$_ss" "$_classic_sm_ol" \
    inject_before "IPrefs" "ClassicRTGScreenMode"
  echo "==> Classic RTG: ScreenMode overlay injected before IPrefs"
else
  echo "==> Classic: No RTG card selected (native chipset)"
  # Only write screenmode.prefs if the selected mode is a native (non-RTG) mode.
  # RTG ModeIDs have $50xx prefix; native ModeIDs have $0001/$0002/$0003 prefix.
  # Writing an RTG ModeID without an RTG card causes Workbench to fail silently.
  if [[ -n "$SCREEN_MODE_ID" ]]; then
    _mode_prefix="${SCREEN_MODE_ID:0:2}"
    if [[ "$_mode_prefix" == "50" ]]; then
      echo "==> Classic: Skipping RTG screenmode.prefs (no RTG card, ModeID=$SCREEN_MODE_ID)"
    else
      write_screenmode_prefs "$SYS_ROOT" "$SCREEN_MODE_ID" "$WB_DEPTH_HEX"
    fi
  fi
fi

# IMP3 prefs: RTG variant for RTG-enabled builds, native for chipset-only
if [[ -n "$RTG_CARD" && "$RTG_CARD" != "none" ]]; then
  install_imp3_prefs "$SYS_ROOT" 1
else
  install_imp3_prefs "$SYS_ROOT" 0
fi

# -----------------------------------------------------------------------------
# Classic Networking: install NIC driver + Roadshow
# -----------------------------------------------------------------------------

classic_network_card_is_a314() {
  [[ "${1:-}" == "A314-CP" ]]
}

classic_a314_pidrive_automount_enabled() {
  [[ "${A314_PIDRIVE_AUTOMOUNT:-1}" != "0" ]]
}

copy_host_binary_with_amiga_perms() {
  local src="$1"
  local dst="$2"
  local tmp_bin="$STAGE/$(basename "$src").$$.$RANDOM"

  cp -f "$src" "$tmp_bin"
  chmod +x "$tmp_bin" 2>/dev/null || true
  write_uaem_with_perms "----rwed" "$tmp_bin"
  fs_copy_file "$tmp_bin" "$dst" --uaemetadata UaeMetafile
  rm -f "$tmp_bin" "${tmp_bin}.uaem" 2>/dev/null || true
}

append_classic_a314_mountlist_entry_if_missing() {
  local target_file="$1"
  local label="$2"
  local source_file="$3"

  if grep -Eqi "^[[:space:]]*${label}" "$target_file" 2>/dev/null; then
    return 0
  fi

  awk -v label="$label" '
    BEGIN { emit = 0 }
    $0 ~ "^[[:space:]]*" label {
      emit = 1
    }
    emit {
      print
      if ($0 ~ /^#$/) exit
    }
  ' "$source_file" >> "$target_file"
  printf "\n" >> "$target_file"
}

merge_classic_a314_mountlist() {
  local sys_root="$1"
  local mountlist_src="$2"
  local stage="$3"
  local existing_mountlist="$stage/a314-mountlist-existing"
  local merged_mountlist="$stage/a314-mountlist-merged"
  local found_existing=0

  rm -f "$existing_mountlist" "$merged_mountlist" 2>/dev/null || true
  if fs_pull_file "$sys_root/Devs/MountList" "$existing_mountlist" >/dev/null 2>&1; then
    found_existing=1
  elif fs_pull_file "$sys_root/Devs/mountlist" "$existing_mountlist" >/dev/null 2>&1; then
    found_existing=1
  fi

  if [[ "$found_existing" -eq 1 ]]; then
    cp -f "$existing_mountlist" "$merged_mountlist"
    make_lf_only "$merged_mountlist"
    printf "\n" >> "$merged_mountlist"

    append_classic_a314_mountlist_entry_if_missing "$merged_mountlist" "PI0:" "$mountlist_src"
    append_classic_a314_mountlist_entry_if_missing "$merged_mountlist" "PD0:" "$mountlist_src"
    append_classic_a314_mountlist_entry_if_missing "$merged_mountlist" "PD1:" "$mountlist_src"
    append_classic_a314_mountlist_entry_if_missing "$merged_mountlist" "PD2:" "$mountlist_src"
    append_classic_a314_mountlist_entry_if_missing "$merged_mountlist" "PD3:" "$mountlist_src"

    make_lf_only "$merged_mountlist"
    fs_copy_file "$merged_mountlist" "$sys_root/Devs/MountList"
    rm -f "$existing_mountlist" "$merged_mountlist" 2>/dev/null || true
    echo "==> Classic: Merged A314 PI0:/PD0:-PD3: entries into Devs/MountList"
    return 0
  fi

  fs_copy_file "$mountlist_src" "$sys_root/Devs/MountList"
  echo "==> Classic: Installed A314 Devs/MountList"
}

resolve_classic_user_startup_path() {
  local sys_root="$1"
  local us_path="S/user-startup"

  if ! fs_dir "$sys_root/S/user-startup"; then
    if fs_dir "$sys_root/S/User-Startup"; then
      us_path="S/User-Startup"
    fi
  fi

  printf "%s\n" "$us_path"
}

write_classic_a314_startup_script() {
  local sys_root="$1"
  local stage="$2"
  local startup_path="$stage/A314-Startup"

  if classic_a314_pidrive_automount_enabled; then
    cat > "$startup_path" <<RSEOF
; Amiga Imager: A314 startup services
FailAt 30
Wait ${A314_BOOT_DELAY_SECS} sec
IF EXISTS C:Mount
  C:Mount PI0: >NIL:
ENDIF
RSEOF
    echo "==> Classic: Wrote S:A314-Startup with ${A314_BOOT_DELAY_SECS}s PI0: mount delay"
  else
    cat > "$startup_path" <<'RSEOF'
; Amiga Imager: A314 startup services
; PI0: auto-mount disabled in app settings
RSEOF
    echo "==> Classic: Wrote S:A314-Startup with PI0: auto-mount disabled"
  fi

  make_lf_only "$startup_path"
  chmod +x "$startup_path"  # Amiga 's' bit: A314-Startup is an AmigaDOS script
  fs_copy_file "$startup_path" "$sys_root/S/A314-Startup"
  rm -f "$startup_path"
}

inject_classic_a314_startup_into_user_startup() {
  local sys_root="$1"
  local stage="$2"
  local us_path
  us_path="$(resolve_classic_user_startup_path "$sys_root")"
  local overlay_path="$stage/User-Startup_A314"

  cat > "$overlay_path" <<'RSEOF'
IF EXISTS S:A314-Startup
  Execute S:A314-Startup
ENDIF
RSEOF
  make_lf_only "$overlay_path"
  inject_overlay_into_script "$sys_root" "$us_path" "$overlay_path" append "" "A314Startup"
  rm -f "$overlay_path"
}

install_classic_a314_driver_bundle() {
  local sys_host="$1"
  local stage="$2"

  local a314_dir="$stage/a314-driver"
  local zip_path="$a314_dir/a314-amiga.zip"
  local extract_dir="$a314_dir/extract"
  rm -rf "$a314_dir" 2>/dev/null || true
  mkdir -p "$extract_dir"

  echo "==> Classic: Downloading A314 Amiga software (${A314_RELEASE_TAG})"
  if ! curl -fSL --retry 2 --connect-timeout 15 -o "$zip_path" "$A314_AMIGA_ZIP_URL" 2>/dev/null; then
    echo "==> WARN: Failed to download A314 bundle from $A314_AMIGA_ZIP_URL"
    rm -rf "$a314_dir" 2>/dev/null || true
    return 1
  fi

  if ! unzip -q -o "$zip_path" -d "$extract_dir" 2>/dev/null; then
    echo "==> WARN: Failed to extract A314 bundle"
    rm -rf "$a314_dir" 2>/dev/null || true
    return 1
  fi

  local cp_device=""
  local eth_device=""
  local pi_command=""
  local a314_fs=""
  local a314_disk_device=""
  local mountlist_file=""
  cp_device="$(find "$extract_dir" -type f -iname 'a314-cp.device' 2>/dev/null | head -1)"
  eth_device="$(find "$extract_dir" -type f -iname 'a314eth.device' 2>/dev/null | head -1)"
  pi_command="$(find "$extract_dir" -type f -iname 'Pi' 2>/dev/null | head -1)"
  a314_fs="$(find "$extract_dir" -type f -iname 'a314fs' 2>/dev/null | head -1)"
  a314_disk_device="$(find "$extract_dir" -type f -iname 'a314disk.device' 2>/dev/null | head -1)"
  mountlist_file="$(find "$extract_dir" -type f -iname 'MountList' 2>/dev/null | head -1)"

  if [[ -z "$cp_device" || -z "$eth_device" ]]; then
    echo "==> WARN: Required A314 drivers not found in downloaded bundle"
    find "$extract_dir" -type f 2>/dev/null | head -20
    rm -rf "$a314_dir" 2>/dev/null || true
    return 1
  fi

  ensure_img_dir "$sys_host/Devs"
  ensure_img_dir "$sys_host/C"
  ensure_img_dir "$sys_host/L"
  copy_host_binary_with_amiga_perms "$cp_device" "$sys_host/Devs/a314.device"
  copy_host_binary_with_amiga_perms "$eth_device" "$sys_host/Devs/a314eth.device"
  echo "==> Classic: Installed A314 drivers to Devs/ (a314.device, a314eth.device)"

  if [[ -n "$a314_fs" && -n "$pi_command" ]]; then
    copy_host_binary_with_amiga_perms "$a314_fs" "$sys_host/L/a314fs"
    copy_host_binary_with_amiga_perms "$pi_command" "$sys_host/C/Pi"
    echo "==> Classic: Installed A314 PiDisk support (L/a314fs, C/Pi)"
  else
    echo "==> WARN: A314 PiDisk support files missing from bundle"
  fi

  if [[ -n "$a314_disk_device" ]]; then
    copy_host_binary_with_amiga_perms "$a314_disk_device" "$sys_host/Devs/a314disk.device"
    echo "==> Classic: Installed A314 disk device (Devs/a314disk.device)"
  else
    echo "==> WARN: A314 disk device missing from bundle"
  fi

  if [[ -n "$mountlist_file" ]]; then
    merge_classic_a314_mountlist "$sys_host" "$mountlist_file" "$stage"
  else
    echo "==> WARN: A314 MountList missing from bundle"
  fi

  write_classic_a314_startup_script "$sys_host" "$stage"

  rm -rf "$a314_dir" 2>/dev/null || true
  return 0
}

write_classic_a314_config() {
  local sys_host="$1"
  local stage="$2"

  [[ -z "${A314_CONFIG_CLOCKPORT_ADDRESS:-}" && -z "${A314_CONFIG_INTERRUPT:-}" ]] && return 0

  local cfg_path="$stage/a314.config"
  : > "$cfg_path"

  if [[ -n "${A314_CONFIG_CLOCKPORT_ADDRESS:-}" ]]; then
    echo "ClockportAddress = ${A314_CONFIG_CLOCKPORT_ADDRESS}" >> "$cfg_path"
  fi
  if [[ -n "${A314_CONFIG_INTERRUPT:-}" ]]; then
    echo "Interrupt = ${A314_CONFIG_INTERRUPT}" >> "$cfg_path"
  fi

  make_lf_only "$cfg_path"
  ensure_img_dir "$sys_host/Devs"
  fs_copy_file "$cfg_path" "$sys_host/Devs/a314.config"
  rm -f "$cfg_path"

  echo "==> Classic: Wrote optional Devs/a314.config"
}

# --- Download and install SANA-II device driver for the selected NIC ---
install_classic_nic_driver() {
  local card="$1"
  local device_name="$2"
  local sys_host="$3"
  local stage="$4"

  [[ "$card" == "none" || -z "$card" ]] && return 0

  if classic_network_card_is_a314 "$card"; then
    install_classic_a314_driver_bundle "$sys_host" "$stage" || return $?
    write_classic_a314_config "$sys_host" "$stage"
    return $?
  fi

  local nic_dir="$stage/nic-driver"
  rm -rf "$nic_dir" 2>/dev/null || true
  mkdir -p "$nic_dir"

  local dl_url=""
  local lha_name=""
  # Device file path inside the extracted LHA (relative)
  local device_path_in_lha=""

  case "$card" in
    Ariadne)
      dl_url="https://amiga.resource.cx/install/Ariadne_v150.lha"
      lha_name="Ariadne_v150.lha"
      device_path_in_lha="ariadne.device"
      ;;
    AriadneII)
      dl_url="https://amiga.resource.cx/install/AriadneII_43_12.lha"
      lha_name="AriadneII_43_12.lha"
      device_path_in_lha="ariadne_ii.device"
      ;;
    A2065)
      dl_url="https://aminet.net/driver/net/a2065v216a.lha"
      lha_name="a2065v216a.lha"
      device_path_in_lha="a2065.device"
      ;;
    Hydra)
      dl_url="https://aminet.net/driver/net/HydraDriver144.lha"
      lha_name="HydraDriver144.lha"
      device_path_in_lha="hydra.device"
      ;;
    X-Surf|X-Surf100)
      dl_url="https://wiki.icomp.de/w/images/3/3a/X-surf-1.16.lha"
      lha_name="X-surf-1.16.lha"
      # v1.16 LHA contains both xsurf.device and xsurf100.device
      if [[ "$card" == "X-Surf100" ]]; then
        device_path_in_lha="x-surf-100.device"
      else
        device_path_in_lha="x-surf.device"
      fi
      ;;
    EB920|LanRover)
      dl_url="https://aminet.net/driver/net/eb920-sanaII.lha"
      lha_name="eb920-sanaII.lha"
      device_path_in_lha="eb920.device"
      ;;
    PCMCIA-NE2000)
      dl_url="https://aminet.net/driver/net/cnetdevice.lha"
      lha_name="cnetdevice.lha"
      device_path_in_lha="cnet.device"
      ;;
    PCMCIA-3Com)
      dl_url="https://aminet.net/driver/net/3c589.lha"
      lha_name="3c589.lha"
      device_path_in_lha="3c589.device"
      ;;
    ZZ9000Net)
      dl_url="https://mntre.com/media/ZZ9000_info_md/ZZ9000Installer1_13.lha"
      lha_name="ZZ9000Installer1_13.lha"
      device_path_in_lha="ZZ9000Net.device"
      ;;
    PicoWiFy)
      dl_url="https://github.com/niklasekstrom/picowyfy/releases/download/v1.3/picowyfy-amiga.lha"
      lha_name="picowyfy-amiga.lha"
      device_path_in_lha="picowyfy.device"
      ;;
    *)
      echo "==> WARN: No driver download source for NIC '$card' — user must supply $device_name"
      return 0
      ;;
  esac

  echo "==> Downloading NIC driver for $card: $lha_name"
  local lha_path="$nic_dir/$lha_name"
  if ! curl -fSL --retry 2 --connect-timeout 15 -o "$lha_path" "$dl_url" 2>/dev/null; then
    echo "==> WARN: Failed to download NIC driver from $dl_url"
    return 1
  fi
  echo "==> Downloaded: $lha_name ($(du -k "$lha_path" | cut -f1) KB)"

  # Extract LHA
  local extract_dir="$nic_dir/extract"
  mkdir -p "$extract_dir"

  local _lha=""
  _lha="$(find_lha)" || true

  if [[ -n "$_lha" ]]; then
    (cd "$extract_dir" && "$_lha" xfq "$lha_path" >/dev/null 2>&1) || true
  else
    # Fallback to hst-imager
    "$HST_IMAGER" fs extract "$lha_path" "$extract_dir" --recursive --force >/dev/null 2>&1 || true
  fi

  # Find the .device file in extracted tree
  local found_device=""
  found_device="$(find "$extract_dir" -iname "$device_path_in_lha" -type f 2>/dev/null | head -1)"

  if [[ -z "$found_device" ]]; then
    # Also try the device_name directly (some LHAs use different naming)
    found_device="$(find "$extract_dir" -iname "$device_name" -type f 2>/dev/null | head -1)"
  fi

  if [[ -n "$found_device" && -f "$found_device" ]]; then
    ensure_img_dir "$sys_host/Devs/Networks"
    fs_copy_file "$found_device" "$sys_host/Devs/Networks/$device_name"
    echo "==> Installed NIC driver: Devs/Networks/$device_name ($(du -k "$found_device" | cut -f1) KB)"
  else
    echo "==> WARN: Could not find $device_path_in_lha in extracted $lha_name"
    echo "==> Contents of extraction:"
    find "$extract_dir" -type f 2>/dev/null | head -20
    return 1
  fi

  # Also install any support libraries shipped alongside the driver.
  # 3c589.lha carries pccard.library in Libs/ — the driver's own Installer
  # script does two copylib steps (pccard.library first, then .device) and
  # the device init fails at OpenLibrary("pccard.library") if it is missing.
  install_nic_driver_libs "$sys_host" "$extract_dir"

  rm -rf "$nic_dir" 2>/dev/null || true
  return 0
}

if [[ -n "$NETWORK_CARD" && "$NETWORK_CARD" != "none" ]]; then
  echo "==> Classic: Installing NIC driver for $NETWORK_CARD ($NETWORK_CARD_DEVICE)"
  install_classic_nic_driver "$NETWORK_CARD" "$NETWORK_CARD_DEVICE" "$SYS_ROOT" "$STAGE" || \
    echo "==> WARN: NIC driver installation failed — network may not work without manual driver install"
fi

if classic_network_card_is_a314 "$NETWORK_CARD" && [[ "$NETWORK_STACK" == "none" ]] && classic_a314_pidrive_automount_enabled; then
  inject_classic_a314_startup_into_user_startup "$SYS_ROOT" "$STAGE"
fi

# CardPatch: A1200 Gayle PCMCIA stability workaround.
# Known HW bug: A1200 PCMCIA cards get stuck and require physical re-seat
# after soft reset / sleep. CardPatch is a small resident patch for
# card.resource that works around it. Only applied on A1200 + PCMCIA NIC.
# See: https://aminet.net/util/boot/CardPatch.lha
install_cardpatch() {
  local sys_root="$1"
  local stage="$2"
  local dl_url="https://aminet.net/util/boot/CardPatch.lha"
  local cp_dir="$stage/cardpatch"
  rm -rf "$cp_dir" 2>/dev/null || true
  mkdir -p "$cp_dir"

  local lha_path="$cp_dir/CardPatch.lha"
  echo "==> Classic: Downloading CardPatch (A1200 PCMCIA stability): $dl_url"
  if ! curl -fSL --retry 2 --connect-timeout 15 -o "$lha_path" "$dl_url" 2>/dev/null; then
    echo "==> WARN: CardPatch download failed — PCMCIA card may need physical re-seat after soft reset"
    rm -rf "$cp_dir" 2>/dev/null || true
    return 1
  fi

  local extract_dir="$cp_dir/extract"
  mkdir -p "$extract_dir"

  local _lha=""
  _lha="$(find_lha)" || true

  if [[ -n "$_lha" ]]; then
    (cd "$extract_dir" && "$_lha" xfq "$lha_path" >/dev/null 2>&1) || true
  else
    "$HST_IMAGER" fs extract "$lha_path" "$extract_dir" --recursive --force >/dev/null 2>&1 || true
  fi

  local found_bin=""
  found_bin="$(find "$extract_dir" -iname "CardPatch" -type f 2>/dev/null | head -1)"
  if [[ -z "$found_bin" ]]; then
    echo "==> WARN: CardPatch binary not found in archive (contents follow)"
    find "$extract_dir" -type f 2>/dev/null | head -20
    rm -rf "$cp_dir" 2>/dev/null || true
    return 1
  fi

  ensure_img_dir "$sys_root/C"
  fs_copy_file "$found_bin" "$sys_root/C/CardPatch"
  echo "==> Classic: Installed C:CardPatch ($(du -k "$found_bin" | cut -f1) KB)"
  rm -rf "$cp_dir" 2>/dev/null || true
  return 0
}

if [[ "${CLASSIC_MACHINE:-}" == "A1200" && "${NETWORK_CARD:-}" == PCMCIA* ]]; then
  echo "==> Classic: A1200 + PCMCIA detected; installing CardPatch for Gayle stability"
  install_cardpatch "$SYS_ROOT" "$STAGE" || \
    echo "==> WARN: CardPatch install failed — PCMCIA may still work but cards can hang after soft reset"
fi

if [[ "$NETWORK_STACK" != "none" ]]; then
  echo "==> Classic: Network stack: $NETWORK_STACK"
  extract_roadshow_bsdsocket "$SYS_ROOT" "$(classic_is_68020plus)"

  # Configure Roadshow interface for the selected network card
  if [[ -n "$NETWORK_CARD_DEVICE" && "$NETWORK_CARD" != "none" ]]; then
    echo "==> Classic: Configuring Roadshow for ${NETWORK_CARD} (${NETWORK_CARD_DEVICE})"
    _go_online="$STAGE/GoOnline"
    if classic_network_card_is_a314 "$NETWORK_CARD"; then
      ensure_img_dir "$SYS_ROOT/Devs/NetInterfaces"
      ensure_img_dir "$SYS_ROOT/Devs/Internet"

      _a314_iface="$STAGE/A314Eth"
      cat > "$_a314_iface" <<RSEOF
device=DEVS:$NETWORK_CARD_DEVICE
address=$A314_AMIGA_IP
netmask=$A314_AMIGA_NETMASK
RSEOF
      make_lf_only "$_a314_iface"
      fs_copy_file "$_a314_iface" "$SYS_ROOT/Devs/NetInterfaces/A314Eth"
      rm -f "$_a314_iface"

      _a314_routes="$STAGE/routes"
      cat > "$_a314_routes" <<RSEOF
default $A314_AMIGA_GATEWAY
RSEOF
      make_lf_only "$_a314_routes"
      fs_copy_file "$_a314_routes" "$SYS_ROOT/Devs/Internet/routes"
      rm -f "$_a314_routes"

      _a314_dns="$STAGE/name_resolution"
      cat > "$_a314_dns" <<RSEOF
nameserver $A314_AMIGA_DNS
RSEOF
      make_lf_only "$_a314_dns"
      fs_copy_file "$_a314_dns" "$SYS_ROOT/Devs/Internet/name_resolution"
      rm -f "$_a314_dns"

      _net_startup="$STAGE/Network-Startup"
      if classic_a314_pidrive_automount_enabled; then
        cat > "$_net_startup" <<'RSEOF'
; Amiga Imager: A314 Roadshow auto-connect
FailAt 30
IF EXISTS S:A314-Startup
  Execute S:A314-Startup
ENDIF
Run >NIL: NetLogViewer CX_POPUP=NO
AddNetInterface DEVS:NetInterfaces/A314Eth QUIET
RSEOF
        echo "==> Classic: Wrote A314 Roadshow config: ${A314_AMIGA_IP}/24 via ${A314_AMIGA_GATEWAY}, DNS ${A314_AMIGA_DNS}"
        echo "==> Classic: Wrote S:Network-Startup to chain through S:A314-Startup"
      else
        cat > "$_net_startup" <<'RSEOF'
; Amiga Imager: A314 Roadshow auto-connect
FailAt 30
Run >NIL: NetLogViewer CX_POPUP=NO
AddNetInterface DEVS:NetInterfaces/A314Eth QUIET
RSEOF
        echo "==> Classic: Wrote A314 Roadshow config: ${A314_AMIGA_IP}/24 via ${A314_AMIGA_GATEWAY}, DNS ${A314_AMIGA_DNS}"
        echo "==> Classic: Wrote S:Network-Startup for A314 Roadshow (PI0: auto-mount disabled)"
      fi
      make_lf_only "$_net_startup"
      chmod +x "$_net_startup"  # Amiga 's' bit: Network-Startup is an AmigaDOS script
      fs_copy_file "$_net_startup" "$SYS_ROOT/S/Network-Startup"
      rm -f "$_net_startup"

      cat > "$_go_online" <<'GOONLINE'
; Amiga Imager: Go Online (A314 / Roadshow)
FailAt 21
C:AddNetInterface DEVS:NetInterfaces/A314Eth QUIET
Echo "A314 network online."
GOONLINE
    else
      ensure_img_dir "$SYS_ROOT/Devs/Internet/Interfaces"
      _rs_tmp="$STAGE/roadshow-ethernet-cfg"
      cat > "$_rs_tmp" <<RSEOF
device=$NETWORK_CARD_DEVICE
unit=0
configure=dhcp
RSEOF
      fs_copy_file "$_rs_tmp" "$SYS_ROOT/Devs/Internet/Interfaces/Ethernet"
      rm -f "$_rs_tmp"
      echo "==> Classic: Wrote Roadshow interface config: $NETWORK_CARD_DEVICE unit=0 dhcp"

      cat > "$_go_online" <<'GOONLINE'
; Amiga Imager: Go Online (Roadshow)
FailAt 21
C:AddNetInterface Devs:Internet/Interfaces/Ethernet
C:ConfigureNetInterface Ethernet UP
Echo "Network online."
GOONLINE
    fi
    make_lf_only "$_go_online"
    chmod +x "$_go_online"  # Amiga 's' bit: GoOnline is an AmigaDOS script
    fs_copy_file "$_go_online" "$SYS_ROOT/S/GoOnline"
    rm -f "$_go_online"

    # Create .info for GoOnline — copy OnetimeRunWB.info as template.
    # It's already Project type with DefaultTool=C:iconx which is exactly
    # what a shell script needs to be launched from WBDock.
    _go_info="$STAGE/GoOnline.info"
    _go_info_src="$ASSETS_DIR/AmigaFiles/System/WBStartup/OnetimeRunWB.info"
    if [[ -f "$_go_info_src" ]]; then
      cp -f "$_go_info_src" "$_go_info"
      fs_copy_file "$_go_info" "$SYS_ROOT/S/GoOnline.info"
      rm -f "$_go_info"
    elif fs_pull_file "$SYS_ROOT/WBStartup/OnetimeRunWB.info" "$_go_info" >/dev/null 2>&1; then
      fs_copy_file "$_go_info" "$SYS_ROOT/S/GoOnline.info"
      rm -f "$_go_info"
    fi
    echo "==> Classic: Created S:GoOnline script + icon for WBDock"
  fi
else
  echo "==> Classic: No network stack selected"
fi

# -----------------------------------------------------------------------------
# 9) Finalize RunAll
# -----------------------------------------------------------------------------
echo "==> PROGRESS 7/10 Installing optional software"
echo "==> Finalize RunAll (must be a file)"

TMP_RUNALL="$STAGE/RunAll"
{
  echo "; Amiga Imager - RunAll optional installers (Classic Amiga)"
  echo "FAILAT 9999"
  echo "ECHO \"Amiga Imager: Running optional installers...\""
  echo "ECHO \"Amiga Imager: RunAll start\" >T:AmigaImager-OptionalInstall.log"

  if [[ -n "$PACKAGES" ]]; then
    IFS=',' read -r -a __PKGS <<< "$PACKAGES"
    for __p in "${__PKGS[@]}"; do
      __p="$(echo "$__p" | tr -d ' \t\n\r')"
      [[ -z "$__p" ]] && continue
      # Skip PiStorm-specific packages in RunAll
      case "$(printf '%s' "$__p" | tr '[:upper:]' '[:lower:]')" in
        videocore*|emu68*|genet*|wifipi*) continue ;;
      esac
      echo ""
      echo "; Package: $__p"
      echo "IF EXISTS \"S:OneTimeRunWB/$__p/Install\""
      echo "  ECHO \"-> $__p\""
      echo "  Execute \"S:OneTimeRunWB/$__p/Install\""
      echo "  IF WARN"
      echo "    ECHO \"WARN: $__p installer returned warning/error; continuing.\""
      echo "  ENDIF"
      echo "ENDIF"
    done
  else
    echo "ECHO \"No packages selected\""
  fi

  echo ""
  echo "ECHO \"Amiga Imager: Optional installers finished.\""
  echo "ECHO \"FALSE\" >ENVARC:FIRSTTIMEBOOT"
  echo "ECHO \"FALSE\" >ENVARC:FIRSTTIMEBOOTWB"
  echo "ECHO \"FALSE\" >ENV:FIRSTTIMEBOOT"
  echo "ECHO \"FALSE\" >ENV:FIRSTTIMEBOOTWB"
} >"$TMP_RUNALL"

make_lf_only "$TMP_RUNALL"
chmod +x "$TMP_RUNALL"  # Amiga 's' bit: required for Execute in OnetimeRunWB
ensure_img_dir "$SYS_ROOT/S/OneTimeRunWB"
fs_copy_file "$TMP_RUNALL" "$SYS_ROOT/S/OneTimeRunWB/RunAll"

# -----------------------------------------------------------------------------
# 10) Ensure WBStartup/OnetimeRunWB exists
# -----------------------------------------------------------------------------
echo "==> Ensure OnetimeRunWB WBStartup script exists"

ASSET_WB_OTR="$ASSETS_DIR/AmigaFiles/System/WBStartup/OnetimeRunWB"
ASSET_WB_OTR_INFO="$ASSETS_DIR/AmigaFiles/System/WBStartup/OnetimeRunWB.info"

fs_mkdir_p "$SYS_ROOT/WBStartup"

TMP_WB_OTR="$STAGE/OnetimeRunWB"
if [[ -f "$ASSET_WB_OTR" ]]; then
  cp -f "$ASSET_WB_OTR" "$TMP_WB_OTR"
else
  cat >"$TMP_WB_OTR" <<'__WB_OTR__'
WAIT 1
IF $FIRSTTIMEBOOTWB EQ "TRUE"
   ECHO "DELETE >NIL: SYS:WBStartup/OneTimeRunWB.info ALL FORCE *nDELETE >NIL: SYS:WBStartup/OneTimeRunWB ALL FORCE" to S:OneTimeRun/DeleteOneTimeRunWB
   ECHO "Amiga Imager - Performing Final Setup"
   c:ce >NIL: S:OneTimeRunWB
   ; Restore disabled WBStartup entries early so they are not lost if optional
   ; installers fail later in this script.
   COPY >NIL: "SYS:WBStartup/Disabled/#?" TO "SYS:WBStartup/" ALL FORCE CLONE
   ECHO "Running One Time Scripts"
   ECHO >T:RunTimeScript "FAILAT 21"
   LIST >>T:RunTimeScriptWB S:OneTimeRunWB PAT=~(#?.info|) LFORMAT="ECHO *"Running Script: %n*"*nEcho *"*"*nExecute *"%p%n*"*nDelete *"%p%n*"*n" ALL FILES
   EXECUTE T:RunTimeScriptWB
   DELETE >NIL: T:RunTimeScriptWB
   DELETE >NIL: "SYS:WBStartup/Disabled" ALL FORCE
   ECHO ""
   ECHO "Rebooting the Amiga in 3 seconds!"
   Wait 3
   Sys:c/diskchange DH0:
   Sys:c/reboot
   ENDCLI
ENDIF
__WB_OTR__
fi
make_lf_only "$TMP_WB_OTR"
chmod +x "$TMP_WB_OTR"  # Amiga 's' bit: required for WBStartup script execution
fs_copy_file_no_makedir "$TMP_WB_OTR" "$SYS_ROOT/WBStartup/OnetimeRunWB"

# Install .info if present in Assets. The icon is do_Type = 4 (Project)
# with DefaultTool = C:iconx — this is correct for a shell script in WBStartup.
# IconX reads the script and executes it via the shell.
if [[ -f "$ASSET_WB_OTR_INFO" ]]; then
  fs_copy_file "$ASSET_WB_OTR_INFO" "$SYS_ROOT/WBStartup/OnetimeRunWB.info"
fi

# Keep other WBStartup items disabled until first Workbench boot.
ensure_img_dir "$SYS_ROOT/WBStartup/Disabled"

# Install additional WBStartup files from Assets into Disabled/
ASSET_WB_DIR="$ASSETS_DIR/AmigaFiles/System/WBStartup"
if [[ -d "$ASSET_WB_DIR" ]]; then
  while IFS= read -r -d '' _wbf; do
    _wbname="$(basename "$_wbf")"
    case "$_wbname" in
      OnetimeRunWB|OnetimeRunWB.info|OneTimeRunWB|OneTimeRunWB.info) continue;;
    esac
    echo "==> WBStartup extra (disabled until first boot): $_wbname"
    fs_copy_file "$_wbf" "$SYS_ROOT/WBStartup/Disabled/$_wbname"
  done < <(find "$ASSET_WB_DIR" -mindepth 1 -maxdepth 1 -type f -print0)
fi

# Ensure WBStartup tools have their executables
for _wbtool in DefIcons AssignWedge; do
  _tmp_wbtool="$STAGE/wbstartup-${_wbtool}"
  if ! fs_pull_file "$SYS_ROOT/WBStartup/Disabled/$_wbtool" "$_tmp_wbtool" >/dev/null 2>&1; then
    if fs_pull_file "$SYS_ROOT/C/$_wbtool" "$_tmp_wbtool" >/dev/null 2>&1; then
      write_uaem_with_perms "----rwed" "$_tmp_wbtool"
      fs_copy_file "$_tmp_wbtool" "$SYS_ROOT/WBStartup/Disabled/$_wbtool" --uaemetadata UaeMetafile
    fi
  fi
done

# Harden first-boot scripts
harden_first_boot_scripts_classic "$SYS_ROOT"

echo "==> PROGRESS 8/10 Installing IBrowse and WBDock"
# -----------------------------------------------------------------------------
# Install IBrowse/AmiSSL (AFTER hardening)
# js_variant: 040fpu for 68040 and below; 060fpu for 68060 (Classic has real FPU).
# -----------------------------------------------------------------------------
_js_variant="040fpu"
[[ "${ACCELERATOR:-}" == "68060" ]] && _js_variant="060fpu"
install_pkg_ibrowse "$SYS_ROOT" "$IBROWSE_DIR" "${IBROWSE_KEY:-}" "$_js_variant"
install_ibrowse_key_file "$SYS_ROOT" "${IBROWSE_KEY:-}" "${IBROWSE_DIR:-}"

# WBDock2
install_pkg_wbdock "$SYS_ROOT" "$IBROWSE_DIR"

# Phase 5: post-install sweep — runs plan actions for any package not yet covered
sweep_plan_post_actions "$SYS_ROOT"

# NewMeter position — slower CPUs need longer delay before repositioning
_nm_delay=5
case "$_cpu_type_for_iconlib" in
  68000)       _nm_delay=40 ;;
  68020|68030) _nm_delay=15 ;;
esac
configure_newmeter_position "$SYS_ROOT" "$_nm_delay"

# IPrefs remains in its standard Startup-Sequence position (after LoadMonDrvs,
# before LoadWB) for both RTG and non-RTG Classic builds. LoadMonDrvs loads the
# P96 monitor, our ClassicRTGScreenMode overlay (injected just before IPrefs)
# applies the RTG mode, and IPrefs reinforces it — all before LoadWB opens.

# --- Re-apply icon positions after Aminet/optional software ---
# The first pass (step 5h) runs before Aminet packages create Programs/ icons.
# This second pass catches those late-arriving .info files.
if [[ "$APPLY_ICON_POSITIONS" == "1" ]]; then
  echo "==> Re-applying icon positions (post-Aminet)"
  apply_icon_positions_pass2
fi

# Auto-arrange Programs icons without curated CSV positions (custom/user packages)
normalize_unpositioned_program_icons "$SYS_ROOT"

verify_programs_entries "$SYS_ROOT"

# -----------------------------------------------------------------------------
# Sanity check
# -----------------------------------------------------------------------------
echo "==> PROGRESS 9/10 Running sanity checks"
echo "==> Sanity check (key dirs on SYS:)"
diag_fs_dir "$SYS_ROOT" || true
diag_fs_dir "$SYS_ROOT/S" --recursive || true
diag_fs_dir "$SYS_ROOT/WBStartup" --recursive || true
diag_fs_dir "$SYS_ROOT/S/OneTimeRunWB" --recursive || true
diag_rdb_info
amigadiskkit_inspect_rdb "$IMG"

# File-vs-directory checks
TMP_CHECK_CLI="$STAGE/.check-System-CLI"
if fs_pull_file "$SYS_ROOT/System/CLI" "$TMP_CHECK_CLI"; then
  echo "==> OK: System/CLI is a file"
elif fs_dir "$SYS_ROOT/System/CLI"; then
  echo "==> WARN: System/CLI is a directory (should be a file)"
else
  echo "==> WARN: System/CLI missing (Workbench Shell icon may fail)"
fi
rm -f "$TMP_CHECK_CLI" 2>/dev/null || true

TMP_CHECK_FONT="$STAGE/.check-Prefs-Font"
TMP_CHECK_FONTZ="$STAGE/.check-Prefs-FontZ"
if fs_pull_file "$SYS_ROOT/Prefs/Font" "$TMP_CHECK_FONT"; then
  echo "==> OK: Prefs/Font is a file"
elif fs_pull_file "$SYS_ROOT/Prefs/Font.Z" "$TMP_CHECK_FONTZ"; then
  echo "==> OK: Prefs/Font.Z is a file"
elif fs_dir "$SYS_ROOT/Prefs/Font"; then
  echo "==> WARN: Prefs/Font is a directory (should be a file)"
elif fs_dir "$SYS_ROOT/Prefs/Font.Z"; then
  echo "==> WARN: Prefs/Font.Z is a directory (should be a file)"
else
  echo "==> WARN: Prefs/Font(.Z) missing"
fi
rm -f "$TMP_CHECK_FONT" "$TMP_CHECK_FONTZ" 2>/dev/null || true

# ALWAYS write OnetimeRunWB to WBStartup at the very end.
# Earlier copies (harden function, section 10) are unreliably lost — the root
# cause is unknown (possibly hst-imager FFS write race). Writing unconditionally
# here as the LAST image operation guarantees the file exists.
echo "==> Final: writing OnetimeRunWB to WBStartup"
_final_otr="$STAGE/OnetimeRunWB.final"
cat > "$_final_otr" <<'__FINAL_OTR__'
; Amiga Imager: first-boot runner
WAIT 1
IF $FIRSTTIMEBOOTWB EQ "TRUE"
   COPY >NIL: "SYS:WBStartup/Disabled/#?" TO "SYS:WBStartup/" ALL FORCE CLONE
   IF EXISTS "S:OneTimeRunWB/RunAll"
      Execute "S:OneTimeRunWB/RunAll" >NIL:
      Delete "S:OneTimeRunWB/RunAll" >NIL:
   ENDIF
   IF EXISTS "S:OneTimeRunWB/localesettings"
      Execute "S:OneTimeRunWB/localesettings" >NIL:
      Delete "S:OneTimeRunWB/localesettings" >NIL:
   ENDIF
   DELETE >NIL: "SYS:WBStartup/Disabled" ALL FORCE
   Wait 1
   Sys:c/diskchange DH0:
   Sys:c/reboot
   ENDCLI
ENDIF
__FINAL_OTR__
make_lf_only "$_final_otr"
chmod +x "$_final_otr"  # Amiga 's' bit: required for WBStartup script execution
ensure_img_dir "$SYS_ROOT/WBStartup"
fs_copy_file "$_final_otr" "$SYS_ROOT/WBStartup/OnetimeRunWB"
echo "==> Final: OnetimeRunWB written ($(wc -c < "$_final_otr") bytes)"
rm -f "$_final_otr"

# Always write .info too
if [[ -f "$ASSETS_DIR/AmigaFiles/System/WBStartup/OnetimeRunWB.info" ]]; then
  fs_copy_file "$ASSETS_DIR/AmigaFiles/System/WBStartup/OnetimeRunWB.info" "$SYS_ROOT/WBStartup/OnetimeRunWB.info"
  echo "==> Final: OnetimeRunWB.info written"
fi

patch_runtime_partition_references "$SYS_ROOT"

echo "==> PROGRESS 10/10 Complete"
echo "==> DONE: $IMG"
