#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# LEGACY — oldbuild-amigaimager-monolith.sh
#
# This is the original monolithic build script, kept for historical reference.
# It was refactored into lib/common.sh + build-pistorm.sh + build-classic.sh
# + build-mister.sh. No platform dispatches to this script anymore.
# Do not edit or rely on this file — use the platform-specific scripts instead.
# ---------------------------------------------------------------------------

set -euo pipefail

# ---------------------------------------------------------------------------
# Amiga-Imager build script version banner
# ---------------------------------------------------------------------------
SCRIPT_VERSION="2026-02-25-rtg-direct-v44"
echo "==> Amiga-Imager build script version: ${SCRIPT_VERSION}" >&2

# Safety: this script requires bash (arrays are used). If you run it via `sh` or `zsh`,
# `set -u` can cause “unbound variable” errors.
if [[ -z "${BASH_VERSION:-}" ]]; then
  echo "ERROR: This script must be run with bash (e.g. 'bash build-emu68-macos.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}"
# Validate: must be a positive integer, else reset to default
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

# build-emu68-macos.sh
# -----------------------------------------------------------------------------
# macOS pipeline aligned to mja65/Amiga Imager-Software / Windows Main.ps1 model:
# - Use ADF directory as primary source (ISO is optional fallback)
# - hst-imager format PiStorm preset (MBR + FAT32 + RDB slice + FFS prepared)
# - Replace default single RDB partition with SDH0 + optional SDH1 (Windows-style)
# - Populate SYS: ONLY from ADFs (from --adf-dir, else ISO/ADF)
# - Ensure FAT has proper Emu68/PiStorm boot bundle (from Assets or auto-download)
# - Overlay Assets/AmigaFiles/System
# - Stage OneTimeRunWB payload and optional packages
# - Ensure LhA exists (Windows parity)
# - Generate RunAll LAST (executed by WBStartup/OnetimeRunWB)
#
# Bash 3.2 compatible (/bin/bash on macOS): no mapfile/readarray/assoc arrays
# -----------------------------------------------------------------------------

echo "==> USING SCRIPT: $0"

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

# Positional arguments (backward compatible):
#   legacy: <img> <kick.rom> <amigaos.iso> <assets_dir>
#   adf-first: <img> <kick.rom> <assets_dir>           (ISO optional)
ISO_PATH=""
ASSETS_DIR=""
# Disambiguation:
# - In adf-first mode, argv[4] is usually the first flag (e.g. --total-gb).
# - In legacy mode, argv[4] is the assets folder path (not a flag).
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=32
FAT_MIB=1024            # Informational; PiStorm preset forces 1GB FAT (not used in formatting)
DH0_MIB=1024
DH1_MIB=0
AMIGA_FS="Ffs"          # Ffs | FfsDC | Pfs3
PFS3_AIO=""
TRANSFER_DIR=""
TRANSFER_PARTITION=""
ADF_DIR=""              # Optional: merged ADF folder
EMU68_BOOT_URL=""       # Optional: override Emu68 boot zip URL
EMU68_BOOT_MODE="auto"  # auto | assets | download
ICON_SET="standard"     # standard | glowicons
PACKAGES=""             # comma-separated ids, staged into SYS:S/OneTimeRunWB/<id>/
LOCALES=""              # comma-separated locale codes (en,de,fr) or empty = all
CSV_DIR=""              # explicit InputFiles directory (overrides find_input_csv search)
AMINET_DIR=""           # staging dir with pre-downloaded .lha files from Aminet
AMINET_RULES_CSV=""     # optional CSV override for Aminet install rules (ListofPackagestoInstall.csv model)
AMINET_RULES_CSV_RESOLVED="" # internal cache: resolved CSV path or "__none__"
DOWNLOAD_ONLY="1"       # 1=ignore Assets programs/packages (download-only mode)
P96_ICOMP_ARCHIVE=""    # optional local Picasso96 iComp archive (.lha)
IBROWSE_DIR=""          # optional staging dir for IBrowse installer (.lha)
IBROWSE_KEY=""          # optional IBrowse key file
INCLUDE_WBDOCK=0        # 1=install WBDock2 from staging dir
WHDLOAD_KICKSTARTS_DIR="" # optional folder with Kickstart ROM files for WHDLoad
IMP3_REG=""              # optional IMP3 registration code (written to S:imp3.reg)
BOING_BAG_1=""          # optional: path to Boing Bag 1 .lha
BOING_BAG_2=""          # optional: path to Boing Bag 2 .lha
HAS_FLOPPY="0"          # 1|0 set ENV:HAS_FLOPPY for DF0 AddBuffers toggle
MOUNT_EMU68BOOT="1"     # 1|0 mount EMU68BOOT partition via DOSDriver (FAT32)
EMU68BOOT_DEVICE="brcm-emmc.device" # brcm-emmc.device (Pi4/CM4) or brcm-sdhc.device (Pi3/Zero2)
APPLY_ICON_POSITIONS="1" # 1|0 apply icon positions from CSV at build time
DETECTED_WB_VERSION="3.2"  # auto-detected from ADF filename
DEBUG=0                # 1 = export Amiga HDF(s) for debugging
DEBUG_EXPORT_DIR=""     # optional: override export directory

# Screen mode (Phase 2)
SCREEN_WIDTH=""
SCREEN_HEIGHT=""
SCREEN_FPS=""
SCREEN_INTERLACE=""
WB_DEPTH=""
SCREEN_MODE_ID=""       # Amiga ModeID hex string, e.g. "50AF1101"
WB_DEPTH_HEX=""         # WB depth as 2-char hex, e.g. "08"
BACKDROP_ENABLED="1"    # WBConfig.prefs backdrop: "1"=enabled, "0"=disabled

# N-partition support (populated from --partition args or legacy --dh0-mib/--dh1-mib)
PART_NAMES=()
PART_LABELS=()
PART_SIZES=()       # numeric MiB or "remaining"
PART_FSTYPES=()     # Ffs | FfsIntl | FfsDC | Pfs3
PART_BOOTABLE=()    # "1" or "0"
PART_BOOTPRI=()     # integer

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

# Unicam (Phase 2)
UNICAM_ENABLE=""
UNICAM_AUTOSTART=""
UNICAM_SCALING=""
UNICAM_BRIGHTNESS=""
UNICAM_CONTRAST=""
UNICAM_SIZEW=""
UNICAM_SIZEH=""
UNICAM_OFFSETX=""
UNICAM_OFFSETY=""

# Network (Phase 2)
NETWORK_STACK=""
ROADSHOW_VARIANT=""   # "demo" or "full" (only when NETWORK_STACK=roadshow)
ROADSHOW_ARCHIVE=""   # optional local Roadshow .lha (full or demo)
WIFI_SSID=""
WIFI_PASSWORD="${WIFI_PASSWORD:-}"  # prefer env var; --wifi-password arg overrides

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

die() { echo "ERROR: $*" >&2; exit 1; }
run() { echo "+ $*"; "$@"; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }

# Escape characters special inside double-quoted strings (for config file generation).
escape_for_dquote() {
  printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/\\$/g; s/`/\\`/g'
}

# Export the Amiga (RDB) partition as an .hdf when DEBUG=1.
# Prefer hst-imager export commands (Windows parity); fall back to copying the raw slice file.
export_debug_hdf() {
  [[ "${DEBUG:-0}" -eq 1 ]] || return 0

  local outdir base out rdb_slice
  outdir="${DEBUG_EXPORT_DIR:-$(dirname "$IMG")}"
  base="$(basename "$IMG")"
  base="${base%.*}"
  out="$outdir/${base}.hdf"
  rdb_slice="$IMG/mbr/2"

  mkdir -p "$outdir" >/dev/null 2>&1 || true

  echo "==> DEBUG: exporting Amiga HDF (MBR partition 2 / RDB slice) to: $out" >&2

  # Try both common syntaxes (hst-imager variants differ)
  if "$HST_IMAGER" mbr part export "$IMG" 2 "$out" >/dev/null 2>&1; then
    echo "==> DEBUG: exported via: hst-imager mbr part export $IMG 2" >&2
    return 0
  fi
  if "$HST_IMAGER" mbr part export "$IMG/mbr" 2 "$out" >/dev/null 2>&1; then
    echo "==> DEBUG: exported via: hst-imager mbr part export $IMG/mbr 2" >&2
    return 0
  fi

  # Fallback: the RDB slice is already present as a raw file under $IMG/mbr/2
  if [[ -f "$rdb_slice" ]]; then
    cp -f "$rdb_slice" "$out" >/dev/null 2>&1 || true
    echo "==> DEBUG: exported via fallback copy of $rdb_slice" >&2
    return 0
  fi

  echo "==> DEBUG: WARN: could not export HDF (no export command + missing $rdb_slice)" >&2
  return 0
}

# --- hst-imager v1.4.x compatibility helpers ---------------------------------
# NOTE: In v1.4.526 `hst-imager fs` supports only: dir, copy, extract, mkdir.
# There is NO fs delete or protect in v1.4.526. Therefore we must avoid workflows that rely on
# deleting a file from the image (e.g. removing Cmdline after a recursive copy).

fs_mkdir_p() {
  # best-effort mkdir (ignore errors if already exists)
  local path="$1"
  "$HST_IMAGER" fs mkdir "$path" >/dev/null 2>&1 || true
}

fs_copy_file_no_makedir() {
  # Copy a single file; NEVER pass --makedir here (it can turn the destination into a directory)
  local src="$1"
  local dst="$2"
  local parent
  parent="$(dirname "$dst")"
  ensure_img_dir "$parent"
  "$HST_IMAGER" fs copy "$src" "$dst" --force
}


stage_onetimerunwb_payload_filtered() {
  # Stage SYS:S/OneTimeRunWB payload from Assets.
  # Uses a single hst-imager --recursive call for speed.
  local src_dir="$1"   # assets source folder
  local dst_dir="$2"   # image destination folder

  [[ -d "$src_dir" ]] || return 0

  echo "==> Stage OneTimeRunWB payload (filtered) from: $src_dir"

  fs_mkdir_p "$dst_dir"

  # Strip macOS metadata into a temp copy then do a single recursive copy.
  local clean_src="$STAGE/.otrwb_clean_$$.$RANDOM"
  cp -a "$src_dir/." "$clean_src"
  find "$clean_src" -name '.DS_Store' -delete 2>/dev/null || true
  find "$clean_src" -name '._*' -delete 2>/dev/null || true
  find "$clean_src" -type d -name '__MACOSX' -exec rm -rf {} + 2>/dev/null || true

  if ! "$HST_IMAGER" fs copy "$clean_src" "$dst_dir" --recursive --force --quiet; then
    "$HST_IMAGER" fs copy "$clean_src" "$dst_dir" --recursive --force
  fi

  rm -rf "$clean_src" 2>/dev/null || true
}

is_skippable_overlay_file() {
  local rel="$1"
  local base lower
  base="$(basename "$rel")"
  lower="$(printf "%s" "$base" | tr '[:upper:]' '[:lower:]')"

  case "$base" in
    .DS_Store|._*|*.DS_Store|__MACOSX|.keep|.keepfile|_UAEFSDB.___) return 0 ;;
  esac

  # Assets include helper docs like "*.info.txt" that must NOT end up on SYS:
  # (e.g. Devs/Monitors/Videocore.info.txt makes BindDrivers try to execute text).
  case "$lower" in
    *.info.txt) return 0 ;;
  esac

  return 1
}

fs_copy_tree_filtered() {
  # Recursively copy a host tree into an image path while skipping non-runtime files.
  # Uses a single hst-imager --recursive call (vs one invocation per file) for speed.
  # $1 = source directory on host
  # $2 = destination directory in image
  local src_dir="$1"
  local dst_dir="$2"

  [[ -d "$src_dir" ]] || return 0

  ensure_img_dir "$dst_dir"

  # Strip macOS metadata from a temporary copy of the source so hst-imager's
  # recursive copy doesn't see .DS_Store / ._* / __MACOSX / *.info.txt files.
  # _UAEFSDB.___ files are kept — hst-imager reads them to set Amiga protection bits.
  local clean_src="$STAGE/.fctf_clean_$$.$RANDOM"
  cp -a "$src_dir/." "$clean_src"
  # Remove macOS junk and overlay-skippable files in-place
  find "$clean_src" -name '.DS_Store' -delete 2>/dev/null || true
  find "$clean_src" -name '._*' -delete 2>/dev/null || true
  find "$clean_src" -type d -name '__MACOSX' -exec rm -rf {} + 2>/dev/null || true
  find "$clean_src" -name '*.info.txt' -delete 2>/dev/null || true
  # Keep _UAEFSDB.___ — hst-imager reads these during fs copy to set Amiga protection bits
  find "$clean_src" -name '.keep' -delete 2>/dev/null || true
  find "$clean_src" -name '.keepfile' -delete 2>/dev/null || true

  # Single recursive copy — dramatically faster than one hst-imager call per file.
  if ! "$HST_IMAGER" fs copy "$clean_src" "$dst_dir" --recursive --force --quiet; then
    # Fallback: retry without --quiet in case that flag causes issues with this version
    "$HST_IMAGER" fs copy "$clean_src" "$dst_dir" --recursive --force
  fi

  rm -rf "$clean_src" 2>/dev/null || true
}

apply_script_bits_for_tree() {
  # Apply Amiga script protection bits to text files copied into the image.
  # $1 = source dir on host (used to read original files)
  # $2 = destination dir in image (where files already exist)
  local src_dir="$1"
  local dst_dir="$2"

  [[ -d "$src_dir" ]] || return 0

  local tmp_dir="$STAGE/scriptbits-$$.$RANDOM"
  rm -rf "$tmp_dir" 2>/dev/null || true
  mkdir -p "$tmp_dir"

  while IFS= read -r -d '' f; do
    local rel base
    rel="${f#$src_dir/}"
    base="$(basename "$rel")"
    case "$base" in
      *.info|*.png) continue ;;
    esac

    # Treat files with NUL bytes as binary; skip those.
    if LC_ALL=C grep -q $'\x00' "$f" 2>/dev/null; then
      continue
    fi

    local tmp_file="$tmp_dir/$rel"
    mkdir -p "$(dirname "$tmp_file")"
    cp "$f" "$tmp_file"
    make_lf_only "$tmp_file"
    write_uaem_with_perms "-s--rwed" "$tmp_file"
    fs_copy_file "$tmp_file" "$dst_dir/$rel" --uaemetadata UaeMetafile
  done < <(find "$src_dir" -type f -print0)

  rm -rf "$tmp_dir" 2>/dev/null || true
}

make_lf_only() {
  local f="$1"
  perl -pi -e 's/\r\n/\n/g; s/\r/\n/g' "$f"
}

fs_copy() {
  local src="$1"; local dst="$2"; shift 2
  echo "+ $HST_IMAGER fs copy $src $dst --force $*"
  if "$HST_IMAGER" fs copy "$src" "$dst" --force "$@"; then
    return 0
  fi
  echo "+ $HST_IMAGER fs copy $src $dst $*"
  "$HST_IMAGER" fs copy "$src" "$dst" "$@"
}

ensure_img_dir() {
  # Create a directory inside the image.
  local dst="$1"
  "$HST_IMAGER" fs mkdir "$dst" >/dev/null 2>&1 || true
  "$HST_IMAGER" fs dir "$dst" >/dev/null 2>&1 || die "Failed to create/access destination drawer: $dst"
}

fs_copy_file() {
  # Copy a single file to an exact destination path inside the image.
  # Never pass --makedir here, otherwise hst-imager can turn the destination into a drawer.
  # Extra args after src/dst are passed through to hst-imager (e.g. --uaemetadata UaeMetafile).
  local src="$1"
  local dst="$2"
  shift 2

  local parent
  parent="$(dirname "$dst")"
  ensure_img_dir "$parent"
  if ! "$HST_IMAGER" fs dir "$parent" >/dev/null 2>&1; then
    die "Failed to create/access destination drawer: $parent"
  fi

  # Retry once after re-creating parent drawer to handle hst-imager path flakiness.
  if fs_copy "$src" "$dst" "$@"; then
    return 0
  fi
  echo "==> WARN: fs copy failed for $dst; re-creating parent and retrying once" >&2
  ensure_img_dir "$parent"
  fs_copy "$src" "$dst" "$@"
}

# Pull a single file from image -> host (works even when `fs extract` is unsupported)
fs_pull_file() {
  local imgpath="$1"
  local localfile="$2"
  mkdir -p "$(dirname "$localfile")"
  "$HST_IMAGER" fs copy "$imgpath" "$localfile" --force >/dev/null 2>&1 && return 0
  "$HST_IMAGER" fs copy "$imgpath" "$localfile"         >/dev/null 2>&1 && return 0
  return 1
}

extract_adf_to_host() {
  local adf="$1"
  local host_dst="$2"

  echo "  -> extracting ADF to host: $(basename "$adf")"
  mkdir -p "$host_dst"

  "$HST_IMAGER" fs extract "$adf" "$host_dst" --recursive --force && return 0
  "$HST_IMAGER" fs extract "$adf" "$host_dst" --recursive        && return 0
  "$HST_IMAGER" fs copy    "$adf" "$host_dst" --recursive --force && return 0
  "$HST_IMAGER" fs copy    "$adf" "$host_dst" --recursive        && return 0

  die "Failed to extract/copy ADF into host dir: $adf"
}

extract_lha_to_host() {
  local lha_file="$1"
  local host_dst="$2"
  echo "  -> extracting LHA to host: $(basename "$lha_file")"
  mkdir -p "$host_dst"

  "$HST_IMAGER" fs extract "$lha_file" "$host_dst" --recursive --force 2>/dev/null && return 0
  "$HST_IMAGER" fs extract "$lha_file" "$host_dst" --recursive        2>/dev/null && return 0
  # Fallback: try lha command (check common locations since PATH may be minimal)
  local _lha=""
  for _l in lha /usr/local/bin/lha /opt/homebrew/bin/lha; do
    command -v "$_l" >/dev/null 2>&1 && _lha="$_l" && break
    [[ -x "$_l" ]] && _lha="$_l" && break
  done
  if [[ -n "$_lha" ]]; then
    (cd "$host_dst" && "$_lha" x "$lha_file") 2>/dev/null && return 0
  fi
  echo "==> WARN: Failed to extract LHA: $(basename "$lha_file")"
  return 1
}

# ---------------------------------------------------------------------------
# parallel_extract_adfs_to_host <host_dst> <adf1> [adf2 ...]
# Extracts multiple ADFs in parallel (up to MAX_PARALLEL_ADF concurrent jobs).
# Falls through to sequential extract_adf_to_host for 0-1 ADFs.
# Dies on failure (after waiting for all in-flight jobs to finish).
# ---------------------------------------------------------------------------
parallel_extract_adfs_to_host() {
  local host_dst="$1"
  shift

  local count=$#
  if [[ $count -eq 0 ]]; then return 0; fi
  if [[ $count -eq 1 ]]; then
    extract_adf_to_host "$1" "$host_dst"
    return 0
  fi

  echo "==> Parallel ADF extraction: $count ADFs, max $MAX_PARALLEL_ADF concurrent"

  local pids=()
  local names=()

  for a in "$@"; do
    (
      set -e
      extract_adf_to_host "$a" "$host_dst"
    ) &
    pids+=($!)
    names+=("$(basename "$a")")

    # Throttle: if at capacity, wait for the oldest job
    if [[ ${#pids[@]} -ge $MAX_PARALLEL_ADF ]]; then
      if ! wait "${pids[0]}"; then
        echo "ERROR: ADF extraction failed: ${names[0]}" >&2
        # Wait for remaining before dying
        for p in "${pids[@]:1}"; do
          wait "$p" 2>/dev/null || true
        done
        die "Parallel ADF extraction failed for: ${names[0]}"
      fi
      pids=("${pids[@]:1}")
      names=("${names[@]:1}")
    fi
  done

  # Drain remaining jobs
  local failed_list=""
  local idx=0
  for pid in "${pids[@]}"; do
    if ! wait "$pid"; then
      failed_list="$failed_list ${names[$idx]}"
    fi
    idx=$((idx + 1))
  done

  if [[ -n "$failed_list" ]]; then
    die "Parallel ADF extraction failed for:$failed_list"
  fi
}

fix_sys_host_file_collisions_from_workbench_adf() {
  # Fix A (HOST-SIDE): ensure certain Workbench core paths are FILES (not drawers)
  # BEFORE we copy the merged SYS payload into the image.
  #
  # Root cause we are fixing:
  # - During the ADF merge, some paths that must be files (Prefs/Font, System/CLI, etc.)
  #   can end up as directories. Since hst-imager v1.4.526 has no fs delete/protect,
  #   fixing this inside the image is unreliable.
  #
  # Approach:
  # - Extract the Workbench ADF to a temp host folder, detect its real root (some
  #   extract modes nest under a volume-name folder), and then overwrite the merged
  #   host payload by replacing those drawers with the correct files.

  local workbench_adf="$1"   # e.g. .../Workbench3.2.adf
  local sys_host="$2"        # host merge dir, e.g. $SYS_HOST

  [[ -f "$workbench_adf" ]] || { echo "==> Fix A: Workbench ADF not found: $workbench_adf (skipping)"; return 0; }
  [[ -d "$sys_host" ]]      || { echo "==> Fix A: SYS host dir missing: $sys_host (skipping)"; return 0; }

  local wb_host="$STAGE/wb-adf-extract"
  rm -rf "$wb_host" >/dev/null 2>&1 || true
  mkdir -p "$wb_host"

  echo "==> Fix A: host-side restore of core Workbench files from $(basename "$workbench_adf")"
  extract_adf_to_host "$workbench_adf" "$wb_host"

  # Detect real root of extracted Workbench ADF.
  # Some extract modes create a single top-level folder (volume label).
  local wb_root="$wb_host"
  if [[ ! -d "$wb_root/System" && ! -d "$wb_root/Prefs" ]]; then
    local topdir_count
    topdir_count="$(find "$wb_host" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
    local topfile_count
    topfile_count="$(find "$wb_host" -mindepth 1 -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')"
    if [[ "$topdir_count" -eq 1 && "$topfile_count" -eq 0 ]]; then
      wb_root="$(find "$wb_host" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
    fi
  fi

  # Paths that MUST be files on SYS: (not drawers)
  local rel
  for rel in \
    "Prefs/Font" "Prefs/Font.info" \
    "System/CLI" "System/CLI.info" \
    "System/Shell" "System/Shell.info" \
    "System/Format" "System/Format.info" \
    "System/Workbench" "System/Workbench.info" \
    ; do

    # If the merged payload has this path as a directory, remove it so a file can exist
    if [[ -d "$sys_host/$rel" ]]; then
      rm -rf "$sys_host/$rel" >/dev/null 2>&1 || true
    fi

    # Copy the file from Workbench extraction if present
    if [[ -f "$wb_root/$rel" ]]; then
      mkdir -p "$(dirname "$sys_host/$rel")"
      cp -f "$wb_root/$rel" "$sys_host/$rel"
    fi
  done

  # Also restore the compressed variants if they exist (some media use .Z)
  for rel in \
    "Prefs/Font.Z" \
    "System/CLI.Z" \
    "System/Shell.Z" \
    "System/Format.Z" \
    "System/Workbench.Z" \
    ; do

    if [[ -d "$sys_host/$rel" ]]; then
      rm -rf "$sys_host/$rel" >/dev/null 2>&1 || true
    fi

    if [[ -f "$wb_root/$rel" ]]; then
      mkdir -p "$(dirname "$sys_host/$rel")"
      cp -f "$wb_root/$rel" "$sys_host/$rel"
    fi
  done

  # Final host-side sanity (this is BEFORE copying into the image)
  for rel in "Prefs/Font" "System/CLI"; do
    if [[ -d "$sys_host/$rel" ]]; then
      echo "==> Fix A: WARN: host payload still has drawer at $rel (expected file)" >&2
    fi
  done
}
fix_sys_host_file_collisions_from_iso() {
  # Fix A.1 (HOST-SIDE): Use ISO root drawers as authoritative source for a few
  # core Workbench binaries that are NOT reliably present when building SYS: only
  # from ADFs (or may collide into drawers during merges).
  #
  # This prevents:
  # - SYS:Prefs/Font becoming a directory
  # - SYS:System/CLI becoming a directory
  # - missing core tools that later trigger requesters

  local iso_root="$1"   # mounted ISO root
  local sys_host="$2"   # host merge dir

  [[ -d "$iso_root" ]] || return 0
  [[ -d "$sys_host" ]] || return 0

  echo "==> Fix A.1: host-side restore of core files from ISO root"

  # helper: copy a file from one of several candidate ISO locations into sys_host/<rel>
  _restore_file() {
    local rel="$1"; shift
    local src=""

    # If target is currently a directory, remove it so a file can exist.
    if [[ -d "$sys_host/$rel" ]]; then
      rm -rf "$sys_host/$rel" >/dev/null 2>&1 || true
    fi

    # Find first existing source
    for cand in "$@"; do
      if [[ -f "$cand" ]]; then
        src="$cand"
        break
      fi
    done

    if [[ -n "$src" ]]; then
      mkdir -p "$(dirname "$sys_host/$rel")"
      cp -f "$src" "$sys_host/$rel"
    fi
  }

  # Prefs/Font prefs program
  _restore_file "Prefs/Font" \
    "$iso_root/Prefs/Font" \
    "$iso_root/Prefs/Font.Z"
  _restore_file "Prefs/Font.info" \
    "$iso_root/Prefs/Font.info"

  # System CLI/Shell/Format
  # Some media store CLI executable under top-level CLI/CLI.
  _restore_file "System/CLI" \
    "$iso_root/System/CLI" \
    "$iso_root/System/CLI.Z" \
    "$iso_root/CLI/CLI" \
    "$iso_root/CLI/CLI.Z"
  _restore_file "System/CLI.info" \
    "$iso_root/System/CLI.info" \
    "$iso_root/CLI/CLI.info"

  _restore_file "System/Shell" \
    "$iso_root/System/Shell" \
    "$iso_root/System/Shell.Z"
  _restore_file "System/Shell.info" \
    "$iso_root/System/Shell.info"

  _restore_file "System/Format" \
    "$iso_root/System/Format" \
    "$iso_root/System/Format.Z"
  _restore_file "System/Format.info" \
    "$iso_root/System/Format.info"

  # System/Workbench (Workbench executable)
  _restore_file "System/Workbench" \
    "$iso_root/System/Workbench" \
    "$iso_root/System/Workbench.Z" \
    "$iso_root/Workbench" \
    "$iso_root/Workbench.Z"
  _restore_file "System/Workbench.info" \
    "$iso_root/System/Workbench.info" \
    "$iso_root/Workbench.info"

  # Fonts drawer: ensure it exists for FONTS: assign. (Non-destructive: only if missing)
  if [[ ! -d "$sys_host/Fonts" && -d "$iso_root/Fonts" ]]; then
    echo "==> Fix A.1: restoring SYS:Fonts drawer from ISO"
    cp -R "$iso_root/Fonts" "$sys_host/Fonts" >/dev/null 2>&1 || true
  fi
}
canonicalize_sys_host_core_paths() {
  # Host-side canonicalization to prevent file-vs-drawer collisions for core OS binaries.
  # Ensures these are FILES (not drawers): System/CLI, Prefs/Font, System/Shell, System/Format.
  # Also ensures SYS:Fonts exists so FONTS: can be assigned.
  local sys_host="$1"
  local iso_root="$2"

  [[ -d "$sys_host" ]] || return 0

  _rm_dir_if_present() {
    local p="$1"
    [[ -d "$p" ]] && rm -rf "$p" >/dev/null 2>&1 || true
  }

  _copy_if_file() {
    local src="$1" dst="$2"
    [[ -f "$src" ]] || return 1
    mkdir -p "$(dirname "$dst")" >/dev/null 2>&1 || true
    cp -f "$src" "$dst" >/dev/null 2>&1 || true
    return 0
  }

  # --- System/CLI ---
  _rm_dir_if_present "$sys_host/System/CLI"
  if [[ ! -f "$sys_host/System/CLI" ]]; then
    # Prefer ISO root (CLI/CLI is common on 3.2 media)
    _copy_if_file "$iso_root/CLI/CLI" "$sys_host/System/CLI" || true
    _copy_if_file "$iso_root/System/CLI" "$sys_host/System/CLI" || true
    # Fall back to any existing compressed or alternate location in merged payload
    _copy_if_file "$sys_host/System/CLI.Z" "$sys_host/System/CLI" || true
    _copy_if_file "$sys_host/CLI/CLI" "$sys_host/System/CLI" || true
    _copy_if_file "$sys_host/CLI/CLI.Z" "$sys_host/System/CLI" || true
  fi

  # .info for CLI (optional, but nice)
  _rm_dir_if_present "$sys_host/System/CLI.info"
  if [[ ! -f "$sys_host/System/CLI.info" ]]; then
    _copy_if_file "$iso_root/CLI/CLI.info" "$sys_host/System/CLI.info" || true
    _copy_if_file "$iso_root/System/CLI.info" "$sys_host/System/CLI.info" || true
    _copy_if_file "$sys_host/CLI/CLI.info" "$sys_host/System/CLI.info" || true
  fi

  # --- Prefs/Font ---
  _rm_dir_if_present "$sys_host/Prefs/Font"
  if [[ ! -f "$sys_host/Prefs/Font" ]]; then
    # Prefer ISO root if present
    _copy_if_file "$iso_root/Prefs/Font" "$sys_host/Prefs/Font" || true
    # Otherwise, duplicate compressed variant if it exists
    _copy_if_file "$sys_host/Prefs/Font.Z" "$sys_host/Prefs/Font" || true
  fi

  # .info for Font
  _rm_dir_if_present "$sys_host/Prefs/Font.info"
  if [[ ! -f "$sys_host/Prefs/Font.info" ]]; then
    _copy_if_file "$iso_root/Prefs/Font.info" "$sys_host/Prefs/Font.info" || true
  fi

  # --- System/Shell + System/Format (best-effort) ---
  for rel in "System/Shell" "System/Format"; do
    _rm_dir_if_present "$sys_host/$rel"
    if [[ ! -f "$sys_host/$rel" ]]; then
      _copy_if_file "$iso_root/$rel" "$sys_host/$rel" || true
      _copy_if_file "$iso_root/${rel}.Z" "$sys_host/$rel" || true
      _copy_if_file "$sys_host/${rel}.Z" "$sys_host/$rel" || true
    fi
    _rm_dir_if_present "$sys_host/${rel}.info"
    if [[ ! -f "$sys_host/${rel}.info" ]]; then
      _copy_if_file "$iso_root/${rel}.info" "$sys_host/${rel}.info" || true
    fi
  done

  # --- System/Workbench (Workbench executable) ---
  _rm_dir_if_present "$sys_host/System/Workbench"
  if [[ ! -f "$sys_host/System/Workbench" ]]; then
    _copy_if_file "$iso_root/System/Workbench" "$sys_host/System/Workbench" || true
    _copy_if_file "$iso_root/System/Workbench.Z" "$sys_host/System/Workbench" || true
    _copy_if_file "$iso_root/Workbench" "$sys_host/System/Workbench" || true
    _copy_if_file "$iso_root/Workbench.Z" "$sys_host/System/Workbench" || true

    # As a last resort, reuse any Workbench binary already merged (exclude Prefs/Workbench).
    local wb_cand=""
    wb_cand="$(find "$sys_host" -type f -name 'Workbench' \
      ! -path "$sys_host/Prefs/*" \
      ! -path "$sys_host/Install/*" \
      2>/dev/null | head -n 1 || true)"
    if [[ -n "$wb_cand" && "$wb_cand" != "$sys_host/System/Workbench" ]]; then
      cp -f "$wb_cand" "$sys_host/System/Workbench" >/dev/null 2>&1 || true
    fi
  fi

  _rm_dir_if_present "$sys_host/System/Workbench.info"
  if [[ ! -f "$sys_host/System/Workbench.info" ]]; then
    _copy_if_file "$iso_root/System/Workbench.info" "$sys_host/System/Workbench.info" || true
    _copy_if_file "$iso_root/Workbench.info" "$sys_host/System/Workbench.info" || true
  fi

  # --- Ensure SYS:Fonts exists (needed for FONTS: assign) ---
  if [[ ! -d "$sys_host/Fonts" && -d "$iso_root/Fonts" ]]; then
    cp -R "$iso_root/Fonts" "$sys_host/Fonts" >/dev/null 2>&1 || true
  fi

  # --- L/fat95 must be a filesystem binary (not a drawer) ---
  _rm_dir_if_present "$sys_host/L/fat95"
  if [[ ! -f "$sys_host/L/fat95" ]]; then
    _copy_if_file "$ASSETS_DIR/AmigaFiles/L/fat95" "$sys_host/L/fat95" || true
    _copy_if_file "$ASSETS_DIR/AmigaFiles/L/FAT95" "$sys_host/L/fat95" || true
    _copy_if_file "$iso_root/L/fat95" "$sys_host/L/fat95" || true
    _copy_if_file "$iso_root/L/FAT95" "$sys_host/L/fat95" || true
  fi

  # Final host-side sanity
  for rel in "System/CLI" "Prefs/Font"; do
    if [[ -d "$sys_host/$rel" ]]; then
      echo "==> WARN: host SYS payload has drawer at $rel (expected file)" >&2
    fi
  done
}

ensure_user_startup_executes_amigaimager_assigns() {
  # Ensure core assigns exist (especially FONTS: -> SYS:Fonts) without clobbering
  # the base Startup-Sequence.
  local sys_root="$1"

  local tmp_assigns="$STAGE/amigaimager-assigns"
  {
    echo "; Amiga Imager assigns (added by macOS build)"
    echo "; Avoid requesters and make Font prefs work on fresh installs."
    echo "Assign >NIL: FONTS: SYS:Fonts"
    echo "Assign >NIL: LOCALE: SYS:Locale"
    echo "Assign >NIL: HELP: SYS:Locale/Help"
    echo "IF EXISTS SYS:System/Installer"
    echo "  Assign >NIL: INSTALLER: SYS:System"
    echo "ENDIF"
    echo "; Mount SD0 (PiStorm FAT boot volume) if DOSDriver present."
    echo "; Pi4vsPi3_Pistorm OneTimeRun renames SD0Pi4/SD0Pi3 -> SD0 at first boot."
    echo "IF EXISTS DEVS:DOSDrivers/SD0"
    echo "  Mount >NIL: DEVS:DOSDrivers/SD0"
    echo "ENDIF"
    echo "; EMU68BOOT: assign for backward compatibility (points to SD0:)"
    echo "IF EXISTS SD0:"
    echo "  Assign >NIL: EMU68BOOT: SD0:"
    echo "ENDIF"
    echo "Assign >NIL: staged: SYS:"
    echo "IF EXISTS SYS:"
    echo "  Assign >NIL: DH0: SYS:"
    echo "ENDIF"
    echo "IF EXISTS Work:"
    echo "  Assign >NIL: DH1: Work:"
    echo "ENDIF"
  } >"$tmp_assigns"
  make_lf_only "$tmp_assigns"

  ensure_img_dir "$sys_root/S"
  fs_copy_file "$tmp_assigns" "$sys_root/S/amigaimager-assigns"

  # Patch (or create) S:user-startup to execute our assigns script.
  local tmp_us="$STAGE/user-startup"
  if fs_pull_file "$sys_root/S/user-startup" "$tmp_us"; then
    :
  elif fs_pull_file "$sys_root/S/User-Startup" "$tmp_us"; then
    :
  else
    printf "; user-startup created by Amiga Imager\n" > "$tmp_us"
  fi

  if ! grep -qi 'amigaimager-assigns' "$tmp_us"; then
    printf "\n; Amiga Imager assigns\nExecute S:amigaimager-assigns\n" >> "$tmp_us"
  fi

  make_lf_only "$tmp_us"
  fs_copy_file "$tmp_us" "$sys_root/S/user-startup"

  # Also patch S:Startup-Sequence (more reliable than relying on user-startup in every environment).
  local tmp_ss="$STAGE/Startup-Sequence"
  if fs_pull_file "$sys_root/S/Startup-Sequence" "$tmp_ss"; then
    local ss_changed=0
    # Avoid DF0: requesters on systems without a floppy drive.
    if ! grep -qi 'Amiga Imager: DF0 AddBuffers toggle' "$tmp_ss"; then
      if grep -Eqi '^[[:space:]]*AddBuffers[[:space:]].*DF0:' "$tmp_ss"; then
        perl -pi -e 's/^(\s*)AddBuffers(.*DF0:.*)$/\1; Amiga Imager: DF0 AddBuffers toggle\n\1IF "$HAS_FLOPPY" EQ "TRUE"\n\1  AddBuffers\2\n\1ENDIF/i' "$tmp_ss"
        ss_changed=1
      fi
    fi

    if ! grep -qi 'amigaimager-assigns' "$tmp_ss"; then
      # Insert before EndCLI if present, otherwise append.
      if grep -qi '^EndCLI' "$tmp_ss"; then
        perl -0777 -pi -e 's/^EndCLI.*$/; Amiga Imager assigns\nExecute S:amigaimager-assigns\n\n$&/mi' "$tmp_ss"
      else
        printf "\n; Amiga Imager assigns\nExecute S:amigaimager-assigns\n" >> "$tmp_ss"
      fi
      ss_changed=1
    fi

    if [[ "$ss_changed" -eq 1 ]]; then
      make_lf_only "$tmp_ss"
      fs_copy_file "$tmp_ss" "$sys_root/S/Startup-Sequence"
    fi
  fi
}

extract_host_to_sys() {
  local host_src="$1"
  local sys_dst="$2"
  echo "==> Copying merged SYS payload from host into image"
  # Use --force directly (no fallback). The fs_copy fallback without --force is
  # broken for recursive copies: the first attempt partially writes files, then
  # the retry without --force immediately fails on the already-written files.
  echo "+ $HST_IMAGER fs copy $host_src/ $sys_dst/ --force --recursive --makedir"
  "$HST_IMAGER" fs copy "$host_src/" "$sys_dst/" --force --recursive --makedir
}

# -----------------------------------------------------------------------------
# inject_overlay_into_script()
#   Inject the content of an overlay file into an AmigaOS script on the image.
#
#   $1 = sys_root       - hst-imager image path to SYS: root
#   $2 = target_script  - relative path within SYS: (e.g. "S/Startup-Sequence")
#   $3 = overlay_file   - host path to overlay content
#   $4 = mode           - inject_after | inject_before | append
#   $5 = marker         - line prefix to match (case-insensitive) for inject_after/inject_before
#   $6 = overlay_name   - tag used in ; BEGIN / ; END comment markers
# -----------------------------------------------------------------------------
inject_overlay_into_script() {
  local sys_root="$1"
  local target_script="$2"
  local overlay_file="$3"
  local mode="$4"
  local marker="$5"
  local overlay_name="$6"

  if [[ ! -f "$overlay_file" ]]; then
    echo "==> Overlay file not found: $overlay_file (skipping $overlay_name)"
    return 0
  fi

  local target_path="$sys_root/$target_script"
  local tmp_target="$STAGE/inject-target"
  local tmp_result="$STAGE/inject-result"
  local tmp_overlay="$STAGE/inject-overlay"

  # Pull current target from image
  if ! fs_pull_file "$target_path" "$tmp_target"; then
    echo "==> WARN: Cannot pull $target_script from image; skipping overlay $overlay_name"
    return 0
  fi

  # Idempotency: skip if BEGIN marker already present (matches both old and new formats)
  if grep -q "${overlay_name}.*BEGIN\|BEGIN.*${overlay_name}" "$tmp_target" 2>/dev/null; then
    echo "==> Overlay $overlay_name already in $target_script; skipping"
    return 0
  fi

  # Prepare overlay content (LF-only)
  cp "$overlay_file" "$tmp_overlay"
  make_lf_only "$tmp_overlay"

  local begin_mark="; ${overlay_name} - Amiga Imager ${SCRIPT_VERSION} - BEGIN"
  local end_mark="; ${overlay_name} - Amiga Imager ${SCRIPT_VERSION} - END"

  # Guard: FAILAT 30 before overlay content, FAILAT 21 after.
  # This prevents commands inside the overlay (e.g. RemLib) from aborting the
  # Startup-Sequence if they return a non-zero code before the script's own
  # FailAt line has been reached.
  local failat_on="FAILAT 30"
  local failat_off="FAILAT 21"

  case "$mode" in
    inject_after)
      # Insert overlay block AFTER the first line whose lowercase starts with $marker
      awk -v marker="$marker" -v bm="$begin_mark" -v em="$end_mark" \
          -v ofile="$tmp_overlay" -v fon="$failat_on" -v foff="$failat_off" '
      BEGIN { done = 0 }
      {
        print
        if (done == 0) {
          low  = tolower($0)
          mlow = tolower(marker)
          if (index(low, mlow) == 1) {
            print ""
            print bm
            print fon
            while ((getline line < ofile) > 0) print line
            close(ofile)
            print foff
            print em
            done = 1
          }
        }
      }' "$tmp_target" > "$tmp_result"
      ;;
    inject_before)
      # Insert overlay block BEFORE the first line whose lowercase starts with $marker
      awk -v marker="$marker" -v bm="$begin_mark" -v em="$end_mark" \
          -v ofile="$tmp_overlay" -v fon="$failat_on" -v foff="$failat_off" '
      BEGIN { done = 0 }
      {
        if (done == 0) {
          low  = tolower($0)
          mlow = tolower(marker)
          if (index(low, mlow) == 1) {
            print bm
            print fon
            while ((getline line < ofile) > 0) print line
            close(ofile)
            print foff
            print em
            print ""
            done = 1
          }
        }
        print
      }' "$tmp_target" > "$tmp_result"
      ;;
    append)
      cp "$tmp_target" "$tmp_result"
      {
        printf "\n%s\n" "$begin_mark"
        printf "%s\n" "$failat_on"
        cat "$tmp_overlay"
        printf "%s\n" "$failat_off"
        printf "%s\n" "$end_mark"
      } >> "$tmp_result"
      ;;
    remove)
      # Remove an existing overlay block (by overlay_name) from the target script
      awk -v name="$overlay_name" '
      BEGIN { skip = 0 }
      {
        if ($0 ~ name && $0 ~ /BEGIN/) { skip = 1; next }
        if ($0 ~ name && $0 ~ /END/)   { skip = 0; next }
        if (skip == 0) print
      }' "$tmp_target" > "$tmp_result"
      ;;
    *)
      echo "==> WARN: Unknown injection mode '$mode' for overlay $overlay_name"
      return 1
      ;;
  esac

  # Check if marker was found (for inject_after/inject_before)
  if [[ "$mode" != "append" && "$mode" != "remove" ]] && ! grep -qF "$begin_mark" "$tmp_result" 2>/dev/null; then
    echo "==> WARN: Marker '$marker' not found in $target_script; appending $overlay_name as fallback"
    cp "$tmp_target" "$tmp_result"
    {
      printf "\n%s\n" "$begin_mark"
      printf "%s\n" "$failat_on"
      cat "$tmp_overlay"
      printf "%s\n" "$failat_off"
      printf "%s\n" "$end_mark"
    } >> "$tmp_result"
  fi

  make_lf_only "$tmp_result"
  fs_copy_file "$tmp_result" "$target_path"
  echo "==> Injected overlay: $overlay_name -> $target_script ($mode${marker:+ '$marker'})"

  rm -f "$tmp_target" "$tmp_result" "$tmp_overlay" 2>/dev/null || true
}

# Check whether a package name appears in the comma-separated $PACKAGES list.
package_is_selected() {
  local pkg="$1"
  # Special case: Roadshow overlay should be injected when network stack is roadshow
  if echo "$pkg" | grep -qi "roadshow" && [[ "${NETWORK_STACK:-}" == "roadshow" ]]; then
    return 0
  fi
  # If user provided paid Picasso96 archive, treat Picasso96 overlay as selected.
  # This keeps startup assigns/parity logic in sync for RTG modes.
  if [[ -n "${P96_ICOMP_ARCHIVE:-}" ]] && echo "$pkg" | grep -qi "picasso96"; then
    return 0
  fi
  # Also check if picasso96.lha is in the Aminet directory (shareware fallback).
  if echo "$pkg" | grep -qi "picasso96" && [[ -n "${AMINET_DIR:-}" && -f "$AMINET_DIR/picasso96.lha" ]]; then
    return 0
  fi
  # If AmiSSL LHA is present in the IBrowse staging directory, treat AmiSSL overlay as selected.
  if [[ -n "${IBROWSE_DIR:-}" && -d "${IBROWSE_DIR:-}" ]] && echo "$pkg" | grep -qi "amissl"; then
    local _ssl_lha
    for _ssl_lha in "${IBROWSE_DIR}"/AmiSSL*.lha; do
      [[ -f "$_ssl_lha" ]] && return 0
    done
  fi
  # Check if Aminet directory contains a .lha whose package hint matches this overlay name.
  if [[ -n "${AMINET_DIR:-}" && -d "${AMINET_DIR:-}" ]]; then
    local _lha
    for _lha in "$AMINET_DIR"/*.lha "$AMINET_DIR"/*.LHA; do
      [[ -f "$_lha" ]] || continue
      local _hint
      _hint="$(aminet_package_hint_for_archive "$(basename "$_lha")" 2>/dev/null || true)"
      if [[ -n "$_hint" ]] && printf '%s\n' "$_hint" | grep -qi "^${pkg}$"; then
        return 0
      fi
    done
  fi
  [[ -n "$PACKAGES" ]] || return 1
  echo ",$PACKAGES," | grep -qi ",$pkg,"
}

# -----------------------------------------------------------------------------
# inject_startup_and_user_startup_overlays()
#   Windows-parity: inject overlay script fragments from Assets/AmigaFiles/System/S/
#   into the base Startup-Sequence and User-Startup on the image.
#
#   Startup-Sequence overlays (always injected if present in Assets):
#     Startup-Sequence_Iconlib     -> inject_after  SetPatch
#     Startup-Sequence_OneTimeRun  -> inject_before BindDrivers
#     Startup-Sequence_UAEGFX     -> inject_after  BindDrivers
#     Startup-Sequence_REXXMAST   -> inject_before BindDrivers
#
#   User-Startup overlays (injected only when matching package is selected):
#     User-Startup_Picasso96      -> append (if Picasso96 in PACKAGES)
#     User-Startup_Roadshow       -> append (if Roadshow in PACKAGES)
#     User-Startup_AmiSSL         -> append (if AmiSSL in PACKAGES)
#     User-Startup_MUI38          -> append (if MUI38 in PACKAGES)
# -----------------------------------------------------------------------------
inject_startup_and_user_startup_overlays() {
  local sys_root="$1"
  local overlay_dir="$2"   # e.g. $SYS_PAYLOAD/S

  [[ -d "$overlay_dir" ]] || { echo "==> No overlay dir at $overlay_dir; skipping script injection"; return 0; }

  echo "==> Injecting script overlays from Assets/System/S/"

  # --- Startup-Sequence overlays (always injected) ---
  local ss="S/Startup-Sequence"

  local f="$overlay_dir/Startup-Sequence_Iconlib"
  [[ -f "$f" ]] && inject_overlay_into_script "$sys_root" "$ss" "$f" inject_after "SetPatch" "Iconlib"

  # REXXMAST must be injected BEFORE OneTimeRun so that ARexx is available
  # for scripts like CheckScreenModeandChipset.rexx that need RexxMast.
  # Both inject before BindDrivers; REXXMAST goes first so it appears
  # earlier in the sequence (inject_before inserts right before the marker).
  f="$overlay_dir/Startup-Sequence_REXXMAST"
  [[ -f "$f" ]] && inject_overlay_into_script "$sys_root" "$ss" "$f" inject_before "BindDrivers" "REXXMAST"

  f="$overlay_dir/Startup-Sequence_OneTimeRun"
  [[ -f "$f" ]] && inject_overlay_into_script "$sys_root" "$ss" "$f" inject_before "BindDrivers" "OneTimeRun"

  f="$overlay_dir/Startup-Sequence_UAEGFX"
  [[ -f "$f" ]] && inject_overlay_into_script "$sys_root" "$ss" "$f" inject_after "BindDrivers" "UAEGFX"

  # --- User-Startup overlays (conditional on selected packages) ---
  local us="S/user-startup"

  # Try both case variants for pulling
  if ! "$HST_IMAGER" fs dir "$sys_root/S/user-startup" >/dev/null 2>&1; then
    if "$HST_IMAGER" fs dir "$sys_root/S/User-Startup" >/dev/null 2>&1; then
      us="S/User-Startup"
    fi
  fi

  for overlay in "$overlay_dir"/User-Startup_*; do
    [[ -f "$overlay" ]] || continue
    local base
    base="$(basename "$overlay")"
    local pkg="${base#User-Startup_}"

    if package_is_selected "$pkg"; then
      inject_overlay_into_script "$sys_root" "$us" "$overlay" append "" "$pkg"
    else
      echo "==> Skipping overlay $base (package $pkg not selected)"
    fi
  done
}

assets_boot_bundle_is_complete() {
  local p="$ASSETS_DIR/AmigaFiles/EMU68Boot"
  [[ -d "$p" ]] || return 1
  local cfg_file kernel_file start_file
  cfg_file="$(find "$p" -maxdepth 1 -type f -iname "config.txt" 2>/dev/null | head -n 1 || true)"
  kernel_file="$(find "$p" -maxdepth 1 -type f -iname "Emu68-pistorm*" 2>/dev/null | head -n 1 || true)"
  start_file="$(find "$p" -maxdepth 1 -type f -iname "start*.elf" 2>/dev/null | head -n 1 || true)"
  [[ -n "$cfg_file" && -n "$kernel_file" && -n "$start_file" ]] && return 0
  return 1
}

resolve_emu68_pistorm_zip_url() {
  if [[ -n "$EMU68_BOOT_URL" ]]; then
    printf "%s\n" "$EMU68_BOOT_URL"
    return 0
  fi

  local api="https://api.github.com/repos/michalsc/Emu68/releases/latest"
  local all_urls url
  all_urls="$(curl -fsSL "$api" \
    | tr -d '\r' \
    | sed -nE 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' \
    | grep -i 'pistorm' \
    | grep -iE '\.zip$' \
    || true)"

  # Prefer PiStorm32Lite payloads first (most common hardware now),
  # then fall back to any PiStorm zip.
  url="$(printf "%s\n" "$all_urls" | grep -i 'pistorm32lite' | grep -i 'emu68' | head -n 1 || true)"
  [[ -n "$url" ]] || url="$(printf "%s\n" "$all_urls" | grep -i 'pistorm32lite' | head -n 1 || true)"
  [[ -n "$url" ]] || url="$(printf "%s\n" "$all_urls" | grep -i 'pistorm16' | grep -i 'emu68' | head -n 1 || true)"
  [[ -n "$url" ]] || url="$(printf "%s\n" "$all_urls" | grep -i 'emu68' | head -n 1 || true)"
  [[ -n "$url" ]] || url="$(printf "%s\n" "$all_urls" | head -n 1 || true)"

  [[ -n "$url" ]] || die "Could not resolve Emu68 PiStorm zip URL from GitHub API. Set --emu68-boot-url explicitly."
  printf "%s\n" "$(printf "%s" "$url" | tr -d ' \t\n')"
}

resolve_emu68_tools_zip_url() {
  local api="https://api.github.com/repos/michalsc/Emu68-tools/releases/latest"
  local url
  url="$(curl -fsSL "$api" \
    | tr -d '\r' \
    | sed -nE 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' \
    | grep -i 'Emu68-tools\.zip' \
    | head -n 1 || true)"
  if [[ -z "$url" ]]; then
    echo "==> WARN: Could not resolve Emu68-tools zip URL from GitHub API." >&2
    return 1
  fi
  printf "%s\n" "$(printf "%s" "$url" | tr -d ' \t\n')"
}

ensure_boot_bundle_kernel_aliases() {
  local boot_stage="$1"

  # Some Emu68 release zips ship only one PiStorm kernel variant.
  # Our config template references all three names, so make safe aliases
  # for missing variants to avoid Raspberry Pi firmware "kernel not found" boot failures.
  local k32 k16 kps fallback
  k32="$(find "$boot_stage" -maxdepth 1 -type f -iname 'Emu68-pistorm32lite' 2>/dev/null | head -n 1 || true)"
  k16="$(find "$boot_stage" -maxdepth 1 -type f -iname 'Emu68-pistorm16' 2>/dev/null | head -n 1 || true)"
  kps="$(find "$boot_stage" -maxdepth 1 -type f -iname 'Emu68-pistorm' 2>/dev/null | head -n 1 || true)"

  fallback="${k32:-${k16:-${kps:-}}}"
  if [[ -z "$fallback" ]]; then
    echo "==> WARN: Boot payload has no Emu68-pistorm* kernel files"
    return 0
  fi

  if [[ -z "$k32" ]]; then
    echo "==> Boot payload: missing Emu68-pistorm32lite, creating alias from $(basename "$fallback")"
    cp -f "$fallback" "$boot_stage/Emu68-pistorm32lite"
  fi
  if [[ -z "$k16" ]]; then
    echo "==> Boot payload: missing Emu68-pistorm16, creating alias from $(basename "$fallback")"
    cp -f "$fallback" "$boot_stage/Emu68-pistorm16"
  fi
  if [[ -z "$kps" ]]; then
    echo "==> Boot payload: missing Emu68-pistorm, creating alias from $(basename "$fallback")"
    cp -f "$fallback" "$boot_stage/Emu68-pistorm"
  fi
}

rdb_partnum_by_name() {
  local info="$1"
  local name="$2"
  echo "$info" | awk -F'|' -v want="$name" '
    BEGIN{inparts=0}
    /^Partitions:/{inparts=1; next}
    inparts {
      gsub(/^[ \t]+|[ \t]+$/, "", $1)
      gsub(/^[ \t]+|[ \t]+$/, "", $2)
      if ($2 == want) { print $1; exit }
    }
    /^Partition table overview:/{exit}
  '
}

# Map partition FS type string to hst-imager DOS type for rdb part add.
# Boot safety: DOS7 (FfsDC) is downgraded to DOS3 for bootable partitions.
part_fs_to_dostype() {
  local fs="$1"
  local is_boot="${2:-0}"
  local dt
  case "$fs" in
    Ffs|FFS)  dt="DOS3" ;;
    FfsIntl)  dt="DOS3" ;;
    FfsDC)    dt="DOS7" ;;
    Pfs3)     dt="PFS3" ;;
    *)        dt="DOS3" ;;
  esac
  # Boot safety: never boot from DOS7 (some Kickstarts hang)
  if [[ "$is_boot" == "1" && "$dt" == "DOS7" ]]; then
    dt="DOS3"
  fi
  echo "$dt"
}

ensure_emu68_config_initramfs_points_to_kick() {
  local fat_root="$1"

  # Skip if we used the Assets config.txt template — it already has correct
  # initramfs lines inside GPIO-conditional sections (PiStorm variant detection).
  # A global replacement would break the stealth firmware and conditional sections.
  if [[ "${USED_ASSETS_CONFIG_TEMPLATE:-0}" -eq 1 ]]; then
    echo "==> initramfs: skipping (Assets template already has correct [ROMPATH] entries)"
    return 0
  fi

  local tmp_cfg="$STAGE/config.txt"

  # Pull current config.txt from FAT. hst-imager v1.4.x can't fs extract individual FAT files reliably.
  if ! fs_pull_file "$fat_root/config.txt" "$tmp_cfg"; then
    echo "==> NOTE: FAT:config.txt not found; creating minimal config.txt with initramfs kick.rom"
    printf "initramfs kick.rom\n" > "$tmp_cfg"
  fi

  # Ensure there is exactly one initramfs line pointing to kick.rom
  if grep -qiE '^[[:space:]]*initramfs[[:space:]]+' "$tmp_cfg"; then
    perl -pi -e 's/^[ \t]*initramfs[ \t]+.*/initramfs kick.rom/i' "$tmp_cfg"
  else
    printf "\ninitramfs kick.rom\n" >> "$tmp_cfg"
  fi

  make_lf_only "$tmp_cfg"
  fs_copy_file "$tmp_cfg" "$fat_root/config.txt"
}

ensure_emu68_cmdline_files() {
  local fat_root="$1"
  local firstboot_extra="${2:-}"

  local tmp_cmd="$STAGE/cmdline.txt"
  local tmp_bak="$STAGE/cmdlineBAK.txt"

  # Baseline cmdline (known-working on PiStorm32lite + CM4):
  #   sd.low_speed / emmc.low_speed = use low-speed SD/eMMC access (reliable on all variants)
  #   sd.unit0=rw / emmc.unit0=rw   = mount SD/eMMC read-write
  #   vc4.mem=16                     = 16MB VideoCore memory (needed for RTG)
  # NOTE: enable_c0_slow + dbf_slowdown are used by CaffeineOS but NOT by the baseline.
  # They will be added back after confirming basic boot works.
  local base="vc4.mem=16 sd.low_speed emmc.low_speed sd.unit0=rw emmc.unit0=rw enable_c0_slow dbf_slowdown"
  local first="$base"

  if [[ -n "$firstboot_extra" ]]; then
    first="$(printf "%s %s\n" "$firstboot_extra" "$base" | awk '{ $1=$1; print }')"
  fi

  printf "%s\n" "$first" > "$tmp_cmd"
  printf "%s\n" "$base"  > "$tmp_bak"
  make_lf_only "$tmp_cmd"
  make_lf_only "$tmp_bak"

  fs_copy_file "$tmp_cmd" "$fat_root/cmdline.txt"
  fs_copy_file "$tmp_bak" "$fat_root/cmdlineBAK.txt"
}

ensure_firsttimeboot_flags_true() {
  local sys_root="$1"
  local tmp1="$STAGE/FIRSTTIMEBOOT"
  local tmp2="$STAGE/FIRSTTIMEBOOTWB"
  local tmp3="$STAGE/HAS_FLOPPY"

  printf "TRUE\n" > "$tmp1"
  printf "TRUE\n" > "$tmp2"
  local floppy="FALSE"
  case "$(printf "%s" "${HAS_FLOPPY:-0}" | tr '[:upper:]' '[:lower:]')" in
    1|true|yes|on) floppy="TRUE" ;;
  esac
  printf "%s\n" "$floppy" > "$tmp3"
  make_lf_only "$tmp1"
  make_lf_only "$tmp2"
  make_lf_only "$tmp3"

  local mk="$STAGE/.mkprefs"
  rm -rf "$mk" >/dev/null 2>&1 || true
  mkdir -p "$mk/Prefs/Env-Archive"
  printf "keep\n" > "$mk/Prefs/Env-Archive/.keep"
  make_lf_only "$mk/Prefs/Env-Archive/.keep"

  fs_copy "$mk/Prefs/" "$sys_root/Prefs/" --recursive --makedir >/dev/null 2>&1 || true

  fs_copy_file "$tmp1" "$sys_root/Prefs/Env-Archive/FIRSTTIMEBOOT"
  fs_copy_file "$tmp2" "$sys_root/Prefs/Env-Archive/FIRSTTIMEBOOTWB"
  fs_copy_file "$tmp3" "$sys_root/Prefs/Env-Archive/HAS_FLOPPY"
}

ensure_hdtoolbox_compat() {
  local sys_root="$1"
  echo "==> Verifying HDToolBox availability"

  # NOTE: hst-imager fs dir returns 0 even for non-existent paths, so we must
  # use fs_pull_file (which does an actual copy) as the reliable existence check.
  local have_binary=0
  local have_icon=0
  local icon_has_coloricon=0
  local _probe="$STAGE/.hdtoolbox-probe"
  local _icon_tmp="$STAGE/.hdtoolbox-icon.info"

  rm -f "$_probe" "$_icon_tmp" 2>/dev/null || true
  if fs_pull_file "$sys_root/Tools/HDToolBox" "$_probe" 2>/dev/null; then
    have_binary=1
  fi
  rm -f "$_probe" 2>/dev/null || true
  if fs_pull_file "$sys_root/Tools/HDToolBox.info" "$_icon_tmp" 2>/dev/null; then
    have_icon=1
    # Check if the .info has Color Icon data (FACE chunk = OS 3.5+ icon)
    if grep -qc 'FACE' "$_icon_tmp" 2>/dev/null; then
      icon_has_coloricon=1
    fi
  fi

  echo "==> HDToolBox probe: binary=$have_binary icon=$have_icon coloricon=$icon_has_coloricon"

  # If binary missing, try to clone from HDTools
  if [[ $have_binary -eq 0 ]]; then
    rm -f "$_probe" 2>/dev/null || true
    if fs_pull_file "$sys_root/Tools/HDTools" "$_probe" 2>/dev/null; then
      rm -f "$_probe" 2>/dev/null || true
      echo "==> HDToolBox missing; cloning SYS:Tools/HDTools -> SYS:Tools/HDToolBox"
      "$HST_IMAGER" fs copy "$sys_root/Tools/HDTools" "$sys_root/Tools/HDToolBox" --force >/dev/null 2>&1 || \
        "$HST_IMAGER" fs copy "$sys_root/Tools/HDTools" "$sys_root/Tools/HDToolBox" >/dev/null 2>&1 || true
      have_binary=1
    else
      echo "==> WARN: SYS:Tools/HDToolBox missing and SYS:Tools/HDTools not found"
    fi
  fi

  # Ensure .info icon exists AND has Color Icon data.
  # Old-style planar-only icons (no FACE/IMAG chunks) display as invisible or
  # blank pages in OS 3.2 Workbench with GlowIcon support.  Replace them with
  # a donor tool icon that has proper Color Icon imagery.
  if [[ $have_binary -eq 1 && ( $have_icon -eq 0 || $icon_has_coloricon -eq 0 ) ]]; then
    local _need_donor=1
    local _reason="missing"
    [[ $have_icon -eq 1 && $icon_has_coloricon -eq 0 ]] && _reason="old-style (no Color Icon)"
    echo "==> HDToolBox.info $_reason; looking for donor icon with Color Icon data"

    # Export existing tooltypes before replacing (if icon exists)
    local _tt_file="$STAGE/.hdtoolbox-tt.txt"
    local _have_tt=0
    if [[ $have_icon -eq 1 ]] && hst_amiga_available; then
      if "$HST_AMIGA" icon tooltypes export "$_icon_tmp" "$_tt_file" >/dev/null 2>&1; then
        _have_tt=1
      fi
    fi

    # Try donor icons in preference order
    local _donor_found=0
    for _donor in "Tools/DiskDoctor.info" "Tools/GraphicDump.info" "Tools/PrepCard.info" "Tools/ShowConfig.info"; do
      rm -f "$_probe" 2>/dev/null || true
      if fs_pull_file "$sys_root/$_donor" "$_probe" 2>/dev/null; then
        if grep -qc 'FACE' "$_probe" 2>/dev/null; then
          echo "==> Using $_donor as Color Icon donor for HDToolBox.info"
          cp -f "$_probe" "$_icon_tmp"
          # Restore HDToolBox tooltypes into the donor icon
          if [[ $_have_tt -eq 1 ]] && hst_amiga_available; then
            "$HST_AMIGA" icon tooltypes import "$_icon_tmp" "$_tt_file" >/dev/null 2>&1 || true
          fi
          # Copy the fixed icon back to the image
          fs_copy_file "$_icon_tmp" "$sys_root/Tools/HDToolBox.info"
          have_icon=1
          _donor_found=1
          break
        fi
      fi
    done

    if [[ $_donor_found -eq 0 ]]; then
      echo "==> WARN: No Color Icon donor found for HDToolBox.info"
    fi
  fi

  rm -f "$_probe" "$_icon_tmp" 2>/dev/null || true
  if [[ $have_binary -eq 1 ]]; then
    echo "==> HDToolBox found in SYS:Tools (icon: $([ $have_icon -eq 1 ] && echo 'yes' || echo 'MISSING'))"
  fi
}

# create_hdtoolbox_pi_variants()
#   Create HDToolboxPi3.info and HDToolboxPi4.info from HDToolBox.info with
#   correct SCSI device tooltypes for each Pi model.
#   Pi4vsPi3_Pistorm renames the matching variant to HDToolbox.info at first boot.
#   $1 = sys_root
create_hdtoolbox_pi_variants() {
  local sys_root="$1"

  local base_info="$STAGE/.hdtoolbox-base.info"
  if ! fs_pull_file "$sys_root/Tools/HDToolBox.info" "$base_info" 2>/dev/null; then
    echo "==> HDToolBox Pi variants: no base icon found (skipping)"
    return 0
  fi

  if ! hst_amiga_available; then
    echo "==> HDToolBox Pi variants: hst-amiga not available (skipping tooltype patching)"
    # Still create copies without tooltype modification as fallback
    fs_copy_file "$base_info" "$sys_root/Tools/HDToolboxPi3.info"
    fs_copy_file "$base_info" "$sys_root/Tools/HDToolboxPi4.info"
    rm -f "$base_info" 2>/dev/null || true
    return 0
  fi

  local pi3_txt="$ASSETS_DIR/AmigaFiles/System/Tools/HDToolBoxPi3.info.txt"
  local pi4_txt="$ASSETS_DIR/AmigaFiles/System/Tools/HDToolBoxPi4.info.txt"

  # Pi3 variant: SCSI_DEVICE_NAME=brcm-sdhc.device
  local pi3_info="$STAGE/.hdtoolbox-pi3.info"
  cp -f "$base_info" "$pi3_info"
  if [[ -f "$pi3_txt" ]]; then
    apply_tooltype_overrides_in_place "$pi3_info" "modify" "$pi3_txt" || true
  fi
  fs_copy_file "$pi3_info" "$sys_root/Tools/HDToolboxPi3.info"
  echo "==> Created SYS:Tools/HDToolboxPi3.info (brcm-sdhc.device)"

  # Pi4 variant: SCSI_DEVICE_NAME=brcm-emmc.device
  local pi4_info="$STAGE/.hdtoolbox-pi4.info"
  cp -f "$base_info" "$pi4_info"
  if [[ -f "$pi4_txt" ]]; then
    apply_tooltype_overrides_in_place "$pi4_info" "modify" "$pi4_txt" || true
  fi
  fs_copy_file "$pi4_info" "$sys_root/Tools/HDToolboxPi4.info"
  echo "==> Created SYS:Tools/HDToolboxPi4.info (brcm-emmc.device)"

  rm -f "$base_info" "$pi3_info" "$pi4_info" 2>/dev/null || true
}

# set_coloricons_borderless_host()
#   Patch all .info files in a HOST directory to set the Borderless flag in
#   their Color Icon FACE chunk.  Operates directly on the filesystem (fast).
#   The FACE chunk layout (OS 3.5+ IFF ICON format):
#     "FACE" (4 bytes) + size (4 big-endian) + width(1) + height(1) + flags(1)
#   Flags bit 0 = Frameless/Borderless.  Setting this bit removes the
#   rectangular border drawn around each icon on the Workbench.
#   $1 = host directory path (e.g. $SYS_HOST)
set_coloricons_borderless_host() {
  local host_dir="$1"
  [[ -d "$host_dir" ]] || return 0

  echo "==> Setting Color Icons borderless in $(basename "$host_dir")"
  python3 -c "
import os, sys, glob
host = sys.argv[1]
patched = 0
total = 0
for info in glob.glob(os.path.join(host, '**', '*.info'), recursive=True):
    total += 1
    try:
        data = bytearray(open(info, 'rb').read())
        pos = data.find(b'FACE')
        if pos < 0:
            continue
        flags_off = pos + 10  # FACE(4) + size(4) + w(1) + h(1)
        if flags_off >= len(data):
            continue
        if data[flags_off] & 0x01:
            continue  # already borderless
        data[flags_off] |= 0x01
        open(info, 'wb').write(data)
        patched += 1
    except Exception:
        pass
print(f'==> Color Icons: {patched}/{total} set to borderless')
" "$host_dir" 2>/dev/null || echo "==> Color Icons: python3 not available (skipping borderless)"
}

# set_coloricons_borderless_image()
#   Same as above but operates on .info files inside an RDB image partition.
#   Only processes icons in specified subdirectories to keep it efficient.
#   $1 = sys_root (image path)
#   Remaining args = subdirectories to process (e.g. "Tools" "System" "Prefs")
set_coloricons_borderless_image() {
  local sys_root="$1"
  shift
  local _dirs=("$@")
  local _patched=0 _total=0
  local _tmp_info="$STAGE/.borderless-img.info"

  for _subdir in "${_dirs[@]}"; do
    local _listing
    _listing="$("$HST_IMAGER" fs dir "$sys_root/$_subdir" --recursive 2>/dev/null || true)"
    while IFS= read -r _line; do
      # Parse hst-imager listing: "Name | Size | ..."
      local _name
      _name="$(echo "$_line" | sed 's/|.*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')"
      echo "$_name" | grep -qiE '\.info$' || continue
      _total=$((_total + 1))

      rm -f "$_tmp_info" 2>/dev/null || true
      fs_pull_file "$sys_root/$_subdir/$_name" "$_tmp_info" 2>/dev/null || continue

      local _face_off
      _face_off="$(python3 -c "
data = open('$_tmp_info','rb').read()
p = data.find(b'FACE')
if p>=0 and p+10<len(data) and not (data[p+10]&1):
    print(p+10)
" 2>/dev/null || true)"
      [[ -z "$_face_off" ]] && continue

      python3 -c "
d=bytearray(open('$_tmp_info','rb').read())
d[$_face_off]|=1
open('$_tmp_info','wb').write(d)
" 2>/dev/null || continue
      fs_copy_file "$_tmp_info" "$sys_root/$_subdir/$_name" 2>/dev/null || continue
      _patched=$((_patched + 1))
    done <<< "$_listing"
  done
  rm -f "$_tmp_info" 2>/dev/null || true
  [[ $_total -gt 0 ]] && echo "==> Color Icons (image): $_patched/$_total set to borderless"
}

# NOTE: This normalization must match the AWK norm() function in
# install_aminet_package_buildtime(). Both strip non-alphanumeric chars
# so that package name matching is case/punctuation-insensitive.
normalize_csv_token() {
  local s="${1:-}"
  s="$(printf "%s" "$s" | tr '[:upper:]' '[:lower:]')"
  s="$(printf "%s" "$s" | tr -cd 'a-z0-9')"
  printf "%s" "$s"
}

resolve_aminet_rules_csv() {
  if [[ -n "$AMINET_RULES_CSV_RESOLVED" ]]; then
    [[ "$AMINET_RULES_CSV_RESOLVED" == "__none__" ]] && return 1
    echo "$AMINET_RULES_CSV_RESOLVED"
    return 0
  fi

  local cand=""
  if [[ -n "$AMINET_RULES_CSV" ]]; then
    [[ -f "$AMINET_RULES_CSV" ]] || return 1
    cand="$AMINET_RULES_CSV"
  else
    cand="$(find_input_csv "ListofPackagestoInstall.CSV" || true)"
  fi

  if [[ -n "$cand" ]]; then
    AMINET_RULES_CSV_RESOLVED="$cand"
    echo "$cand"
    return 0
  fi

  AMINET_RULES_CSV_RESOLVED="__none__"
  return 1
}

aminet_package_hint_for_archive() {
  local lha_base="$1"
  local lower
  lower="$(printf '%s' "$lha_base" | tr '[:upper:]' '[:lower:]')"
  case "$lower" in
    roadie.lha) echo "Roadie" ;;
    mui38usr.lha) echo "MUI38" ;;
    hippoplayer.lha) echo "HippoPlayer" ;;
    imp3.lha) echo "IMP3" ;;
    picasso96.lha) echo "Picasso96" ;;
    reqtoolsusr.lha|reqtoolswide.lha|reqtools-wide.lha) echo "ReqTools" ;;
    guigfxlib.lha) echo "guigfxlib" ;;
    renderlib.lha) echo "renderlib" ;;
    toolsdaemon21a.lha) echo "ToolsDaemon" ;;
    sysinfo.lha) echo "SysInfo" ;;
    dopus414jrbin.lha|dopus416jrbin.lha) echo "DOpus416" ;;
    whdload_usr.lha|whdload-user.lha|whdload.lha) echo "WHDLoad" ;;
    remlib.lha) echo "RemLib" ;;
    cpublit.lha) echo "CpuBlit" ;;
    patchram.lha) echo "PatchRAM" ;;
    noclick.lha) echo "NoClick" ;;
    lha_68040.lha|lha_68020.lha|lha_68k.lha) echo "LhA" ;;
    ahiusr_4.18.lha) echo "AHI" ;;
    poseidonmain.lha) echo "Poseidon" ;;
    akpng-dt.lha) echo "akPNG" ;;
    akgif-dt.lha) echo "akGIF" ;;
    akjfif-dt.lha) echo "akJFIF" ;;
    tankmouse.lha) echo "TankMouse" ;;
    hid2ami.lha) echo "Hid2Ami" ;;
    wbdock2.lha) echo "WBDock2" ;;
    iconlib_46.4.lha) echo "IconLib" ;;
    xadmaster020.lha) echo "xadmaster" ;;
    uhctools.lha) echo "UHCTools" ;;
    newmeter.lha) echo "NewMeter" ;;
    *) return 1 ;;
  esac
}

harden_first_boot_scripts() {
  local sys_root="$1"

  echo "==> Hardening first-boot scripts (UAE/PiStorm behavior)"

  # 1) Startup-Sequence:
  # - If System was not detected, default to UAE (FS-UAE often lacks UAEMAJOR env var).
  # - Skip *_Pistorm scripts in UAE regardless of filename case.
  # - Guard optional RemLib call.
  local tmp_ss="$STAGE/Startup-Sequence.harden"
  if fs_pull_file "$sys_root/S/Startup-Sequence" "$tmp_ss"; then
    local ss_changed=0

    if ! grep -q 'Amiga Imager fallback: default System=UAE' "$tmp_ss"; then
      awk '
        BEGIN { inserted = 0 }
        {
          if (inserted == 0 && $0 ~ /^IF \$KICKVER EQ 47/) {
            print "; Amiga Imager fallback: default System=UAE when detection did not set it"
            print "IF \"$System\" EQ \"\""
            print "  SETENV System \"UAE\""
            print "  SETENV SYSTEM \"UAE\""
            print "ENDIF"
            print ""
            inserted = 1
          }
          print
        }
      ' "$tmp_ss" > "$tmp_ss.new" && mv "$tmp_ss.new" "$tmp_ss"
      ss_changed=1
    fi

    # Handle both full "_pistorm" and truncated "_pist" names (FFS 30-char limit).
    sed \
      -e 's/PAT=~(#?.info|#?_pistorm)/PAT=~(#?.info|#?_pistorm|#?_Pistorm|#?_PISTORM|#?_pist|#?_Pist|#?_PIST)/g' \
      -e 's/PAT=~(#?.info|#?_pistorm|#?_Pistorm|#?_PISTORM)/PAT=~(#?.info|#?_pistorm|#?_Pistorm|#?_PISTORM|#?_pist|#?_Pist|#?_PIST)/g' \
      "$tmp_ss" > "$tmp_ss.new"
    if ! cmp -s "$tmp_ss" "$tmp_ss.new"; then
      mv "$tmp_ss.new" "$tmp_ss"
      ss_changed=1
    else
      rm -f "$tmp_ss.new"
    fi

    if grep -q 'C:RemLib >NIL: icon.library' "$tmp_ss"; then
      awk '
        {
          if ($0 ~ /C:RemLib >NIL: icon.library/) {
            print "  IF EXISTS C:RemLib"
            print "    C:RemLib >NIL: icon.library"
            print "  ENDIF"
            next
          }
          print
        }
      ' "$tmp_ss" > "$tmp_ss.new" && mv "$tmp_ss.new" "$tmp_ss"
      ss_changed=1
    fi

    # Keep local $System in sync with SETENV so later checks are accurate.
    if ! grep -q 'Amiga Imager: sync System var' "$tmp_ss"; then
      awk '
        function emit_set(val, indent) { print indent "SET System \"" val "\"" }
        {
          print
          if ($0 ~ /SETENV[[:space:]]+System[[:space:]]+\"PiStorm\"/) {
            match($0, /^[[:space:]]*/); emit_set("PiStorm", substr($0, RSTART, RLENGTH))
          } else if ($0 ~ /SETENV[[:space:]]+System[[:space:]]+\"UAE\"/ || $0 ~ /SETENV[[:space:]]+SYSTEM[[:space:]]+\"UAE\"/) {
            match($0, /^[[:space:]]*/); emit_set("UAE", substr($0, RSTART, RLENGTH))
          }
        }
      ' "$tmp_ss" > "$tmp_ss.new"
      if ! cmp -s "$tmp_ss" "$tmp_ss.new"; then
        mv "$tmp_ss.new" "$tmp_ss"
        # Tag to avoid reapplying if already present.
        printf "\n; Amiga Imager: sync System var\n" >> "$tmp_ss"
        ss_changed=1
      else
        rm -f "$tmp_ss.new"
      fi
    fi

    # Remove deprecated EMU68INFO Protect block (caused requesters on some systems).
    if grep -q 'Amiga Imager: ensure EMU68INFO executable' "$tmp_ss"; then
      awk '
        BEGIN { skip = 0 }
        {
          if ($0 ~ /; Amiga Imager: ensure EMU68INFO executable/) { skip = 1; next }
          if (skip == 1) {
            if ($0 ~ /^ENDIF/) { skip = 0; next }
            next
          }
          print
        }
      ' "$tmp_ss" > "$tmp_ss.new" && mv "$tmp_ss.new" "$tmp_ss"
      ss_changed=1
    fi

    # Early fallback: EMU68BOOT: -> SYS: before SD0 is mounted (amigaimager-assigns updates to SD0: later).
    if ! grep -q 'Amiga Imager: EMU68BOOT assign' "$tmp_ss"; then
      awk '
        BEGIN { inserted = 0 }
        {
          if (inserted == 0 && $0 ~ /^c:ce >NIL: S:OneTimeRun/) {
            print "; Amiga Imager: EMU68BOOT assign"
            print "Assign >NIL: EMU68BOOT: SYS: ADD"
            print ""
            inserted = 1
          }
          print
        }
      ' "$tmp_ss" > "$tmp_ss.new" && mv "$tmp_ss.new" "$tmp_ss"
      ss_changed=1
    fi

    if [[ "$ss_changed" -eq 1 ]]; then
      make_lf_only "$tmp_ss"
      fs_copy_file "$tmp_ss" "$sys_root/S/Startup-Sequence"
    fi
  fi

  # 2) OnetimeRunWB launcher:
  #    run only known scripts (Cmdline/localesettings/RunAll) so package payload
  #    files (.lha, metadata) are never executed as scripts.
  #    All output suppressed (>NIL:) so no console window opens during WBStartup —
  #    an open window would cause Intuition to show a requester when IPrefs applies
  #    the RTG screen mode.
  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/Cmdline"
      Execute "S:OneTimeRunWB/Cmdline" >NIL:
      Delete "S:OneTimeRunWB/Cmdline" >NIL:
   ENDIF

   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 SDH0:
   IF $System EQ "UAE"
      Sys:c/reboot
   ELSE
      c:EMU68INFO HARDRESET
   ENDIF
   ENDCLI
ENDIF
__ONETIMERUNWB_HARDEN__
  make_lf_only "$tmp_wb"
  fs_copy_file "$tmp_wb" "$sys_root/WBStartup/OnetimeRunWB"

  # 3) Cmdline first-boot helper: no-op on UAE and when backup file is missing.
  local tmp_cmd="$STAGE/Cmdline.harden"
  if fs_pull_file "$sys_root/S/OneTimeRunWB/Cmdline" "$tmp_cmd"; then
    cat > "$tmp_cmd" <<'__CMDLINE_HARDEN__'
; Amiga Imager hardening: only restore cmdline on real PiStorm boot.
IF "$System" EQ "UAE"
   SKIP END
ENDIF
IF NOT EXISTS EMU68BOOT:cmdlineBAK.txt
   SKIP END
ENDIF
IF EXISTS EMU68BOOT:cmdline.txt
   DELETE EMU68BOOT:cmdline.txt QUIET
ENDIF
RENAME FROM EMU68BOOT:cmdlineBAK.txt TO EMU68BOOT:cmdline.txt QUIET
LAB END
__CMDLINE_HARDEN__
    make_lf_only "$tmp_cmd"
    fs_copy_file "$tmp_cmd" "$sys_root/S/OneTimeRunWB/Cmdline"
  fi

  # 4) PiStorm screen/chipset probe:
  #    Script runs in CLI phase (before Workbench) so ScreenMode USE has no open
  #    window to close — matches PC reference behaviour, no requester possible.
  # 5) 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 codecs directory has actual files (hst-imager fs dir returns 0 even for non-existent paths)
    local codecs_output
    codecs_output="$("$HST_IMAGER" fs dir "$sys_root/Programs/Ibrowse/codecs" 2>/dev/null || true)"
    local has_codecs=0
    if echo "$codecs_output" | grep -q "gif\.codec\|jpeg\.codec\|png\.codec"; then
      has_codecs=1
    fi
    echo "==> Hardening: IBrowse codecs present = $has_codecs"
    if [[ "$has_codecs" -eq 1 ]]; then
      # Codecs exist in image - write guarded script that patches them at first boot.
      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
      # No codecs in image - replace with no-op to suppress boot errors.
      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"
    fs_copy_file "$tmp_ib" "$sys_root/S/OneTimeRun/Ibrowse"
  fi
}

# -----------------------------------------------------------------------------
# Parse flags
# -----------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    --total-gb)        TOTAL_GB="${2:?}"; shift 2;;
    --fat-mib)          FAT_MIB="${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;;
    --hst-imager)       HST_IMAGER="${2:?}"; shift 2;;
    --transfer-dir)     TRANSFER_DIR="${2:?}"; shift 2;;
    --transfer-partition) TRANSFER_PARTITION="${2:?}"; shift 2;;
    --adf-dir)          ADF_DIR="${2:?}"; shift 2;;
    --emu68-boot-url)   EMU68_BOOT_URL="${2:?}"; shift 2;;
    --emu68-boot-mode)  EMU68_BOOT_MODE="${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;;
    --aminet-rules-csv) AMINET_RULES_CSV="${2:?}"; shift 2;;
    --download-only)    DOWNLOAD_ONLY="${2:?}"; shift 2;;
    --p96-icomp-archive) P96_ICOMP_ARCHIVE="${2:?}"; shift 2;;
    --has-floppy)       HAS_FLOPPY="${2:?}"; shift 2;;
    --mount-emu68boot)  MOUNT_EMU68BOOT="${2:?}"; shift 2;;
    --emu68boot-device) EMU68BOOT_DEVICE="${2:?}"; shift 2;;
    --debug)            DEBUG=1; shift 1;;
    --debug-export-dir) DEBUG_EXPORT_DIR="${2:?}"; shift 2;;
    --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;;
    --wb-depth)         WB_DEPTH="${2:?}"; shift 2;;
    --screen-mode-id)   SCREEN_MODE_ID="${2:?}"; shift 2;;
    --wb-depth-hex)     WB_DEPTH_HEX="${2:?}"; shift 2;;
    --backdrop-enabled) BACKDROP_ENABLED="${2:?}"; shift 2;;
    --hst-amiga)        HST_AMIGA="${2:?}"; shift 2;;
    --unicam-enable)    UNICAM_ENABLE="${2:?}"; shift 2;;
    --unicam-autostart) UNICAM_AUTOSTART="${2:?}"; shift 2;;
    --unicam-scaling)   UNICAM_SCALING="${2:?}"; shift 2;;
    --unicam-brightness) UNICAM_BRIGHTNESS="${2:?}"; shift 2;;
    --unicam-contrast)  UNICAM_CONTRAST="${2:?}"; shift 2;;
    --unicam-sizew)     UNICAM_SIZEW="${2:?}"; shift 2;;
    --unicam-sizeh)     UNICAM_SIZEH="${2:?}"; shift 2;;
    --unicam-offsetx)   UNICAM_OFFSETX="${2:?}"; shift 2;;
    --unicam-offsety)   UNICAM_OFFSETY="${2:?}"; shift 2;;
    --network-stack)    NETWORK_STACK="${2:?}"; shift 2;;
    --roadshow-variant) ROADSHOW_VARIANT="${2:?}"; shift 2;;
    --roadshow-archive) ROADSHOW_ARCHIVE="${2:?}"; shift 2;;
    --wifi-ssid)        WIFI_SSID="${2:?}"; shift 2;;
    --wifi-password)    WIFI_PASSWORD="${2:?}"; shift 2;;
    --ibrowse-dir)      IBROWSE_DIR="${2:?}"; shift 2;;
    --ibrowse-key)      IBROWSE_KEY="${2:?}"; shift 2;;
    --include-wbdock)   INCLUDE_WBDOCK=1; shift;;
    --whdload-kickstarts-dir) WHDLOAD_KICKSTARTS_DIR="${2:?}"; shift 2;;
    --imp3-reg)             IMP3_REG="${2:?}"; shift 2;;
    --boing-bag-1)          BOING_BAG_1="${2:?}"; shift 2;;
    --boing-bag-2)          BOING_BAG_2="${2:?}"; shift 2;;
    --partition)
      IFS=':' read -r _pn _pl _ps _pf _pb _pp <<< "${2:?}"
      PART_NAMES+=("$_pn")
      PART_LABELS+=("$_pl")
      PART_SIZES+=("$_ps")
      PART_FSTYPES+=("${_pf:-Ffs}")
      PART_BOOTABLE+=("${_pb:-0}")
      PART_BOOTPRI+=("${_pp:-0}")
      shift 2;;
    *) die "Unknown argument: $1";;
  esac
done

# Normalize download-only flag (default: 1). When enabled, ignore Assets packages.
if [[ "${DOWNLOAD_ONLY:-0}" != "1" ]]; then
  DOWNLOAD_ONLY="0"
fi
if [[ "$DOWNLOAD_ONLY" == "1" ]]; then
  PACKAGES=""
fi

# -----------------------------------------------------------------------------
# Normalize filesystem token for hst-imager
# -----------------------------------------------------------------------------
# IMPORTANT boot-safety rule:
# - If user selects FfsDC (DOS7), we still format the *boot* partition (SDH0) as DOS3.
#   This avoids Kickstart variants that cannot boot DOS7 (symptom: Kickstart screen hang).
# - Data partition (SDH1) keeps the selected FS.
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
# -----------------------------------------------------------------------------
[[ -f "$KICK_ROM" ]] || die "Kickstart ROM missing: $KICK_ROM"
[[ -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
  die "No input media available. Provide --adf-dir (preferred) or an ISO path as 3rd positional argument."
fi

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

case "$EMU68_BOOT_MODE" in
  auto|assets|download) ;;
  *) die "Invalid --emu68-boot-mode. Use: auto | assets | download" ;;
esac

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

case "$(printf "%s" "$MOUNT_EMU68BOOT" | tr '[:upper:]' '[:lower:]')" in
  1|true|yes|on) MOUNT_EMU68BOOT="1" ;;
  0|false|no|off|"") MOUNT_EMU68BOOT="0" ;;
  *) die "Invalid --mount-emu68boot. Use: 0 | 1" ;;
esac

case "$(printf "%s" "$EMU68BOOT_DEVICE" | tr '[:upper:]' '[:lower:]')" in
  brcm-sdhc.device|sdhc) EMU68BOOT_DEVICE="brcm-sdhc.device" ;;
  brcm-emmc.device|emmc) EMU68BOOT_DEVICE="brcm-emmc.device" ;;
  *) die "Invalid --emu68boot-device. Use: brcm-sdhc.device | brcm-emmc.device" ;;
esac

# --- N-partition support: populate partition arrays from --partition args or legacy fallback ---
if [[ ${#PART_NAMES[@]} -eq 0 ]]; then
  # Legacy mode: build from --dh0-mib/--dh1-mib
  PART_NAMES=("SDH0")
  PART_LABELS=("Workbench")
  PART_SIZES=("$DH0_MIB")
  PART_FSTYPES=("$AMIGA_FS")
  PART_BOOTABLE=("1")
  PART_BOOTPRI=("1")
  if [[ "$DH1_MIB" -gt 0 ]]; then
    PART_NAMES+=("SDH1")
    PART_LABELS+=("Work")
    PART_SIZES+=("remaining")
    PART_FSTYPES+=("$AMIGA_FS")
    PART_BOOTABLE+=("0")
    PART_BOOTPRI+=("0")
  fi
else
  # N-partition mode: override HST_FS from boot partition
  case "${PART_FSTYPES[0]}" in
    Pfs3|PFS3)
      HST_FS="Pfs3"
      AMIGA_FS="Pfs3"
      ;;
    FfsDC)
      HST_FS="DOS7"
      AMIGA_FS="FfsDC"
      ;;
    *)
      HST_FS="$(part_fs_to_dostype "${PART_FSTYPES[0]}" "0")"
      AMIGA_FS="Ffs"
      ;;
  esac
fi
NUM_PARTS=${#PART_NAMES[@]}

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

DISK_SIZE_BYTES_TMP=$(( TOTAL_GB * 1000 * 1000 * 1000 )); IMAGE_SIZE="$DISK_SIZE_BYTES_TMP"

DISK_SIZE_BYTES=$(( TOTAL_GB * 1000 * 1000 * 1000 ))

echo "==> Layout:"
echo "    Total: ${TOTAL_GB} GB"
echo "    FAT:   ${FAT_MIB} MiB (PiStorm preset forces 1GB minimum)"
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]} 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 "    Boot:  mode=${EMU68_BOOT_MODE} url=${EMU68_BOOT_URL:-<auto latest pistorm>}"
echo "    Icons: ${ICON_SET} (source ADF selected automatically: 4ColorIcons or GlowIcons)"
echo "    Locales: ${LOCALES:-<all>}"
echo "    Pkgs:  ${PACKAGES:-<none>}"
echo "    Download-only: ${DOWNLOAD_ONLY}"
echo "    P96:   ${P96_ICOMP_ARCHIVE:-<none>}"
echo "    Debug: ${DEBUG} (export-dir=${DEBUG_EXPORT_DIR:-<img directory>})"

# -----------------------------------------------------------------------------
# Stage + ISO mount
# -----------------------------------------------------------------------------
STAGE="$(mktemp -d "/tmp/emu68-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)"
  hdiutil attach "$ISO_PATH" -readonly -nobrowse -mountpoint "$MNT" >/dev/null
  ISO_ROOT="$MNT"
  ISO_MOUNTED=1
else
  echo "==> No ISO provided; using ADF-only mode"
fi

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

find_ffs_in_adf_dir() {
  # Try to locate FastFileSystem by extracting ADFs to a temp host folder.
  # Echoes a host path to the discovered binary on success.
  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

  # Prefer Workbench/Install disks first, then try all ADFs.
  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

  # De-duplicate while preserving order.
  local -a deduped=()
  local seen="|"
  local item
  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"

    if ! "$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 cand
    for cand in \
      "$adf_out/L/FastFileSystem" \
      "$adf_out/L/fastfilesystem" \
      "$adf_out/FastFileSystem" \
      "$adf_out/fastfilesystem"
    do
      if [[ -f "$cand" ]]; then
        local host_ffs="$tmp_ffs_root/FastFileSystem"
        cp -f "$cand" "$host_ffs" >/dev/null 2>&1 || host_ffs="$cand"
        echo "$host_ffs"
        return 0
      fi
    done
  done

  return 1
}

# Prefer ADFs first (explicit --adf-dir, then ISO/ADF), fall back to ISO root.
ADF_FFS_SRC=""
if [[ -n "$ADF_DIR" && -d "$ADF_DIR" ]]; then
  ADF_FFS_SRC="$ADF_DIR"
elif [[ -d "$ISO_ROOT/ADF" ]]; then
  ADF_FFS_SRC="$ISO_ROOT/ADF"
fi

if [[ -n "$ADF_FFS_SRC" ]]; then
  echo "==> FastFileSystem: scanning ADFs first ($ADF_FFS_SRC)"
  FFS_PATH="$(find_ffs_in_adf_dir "$ADF_FFS_SRC" || true)"
fi

if [[ -z "$FFS_PATH" ]]; then
  # Check standard ISO root, then OS 3.9 CD paths
  for ffs_candidate in \
    "$ISO_ROOT/L/FastFileSystem" \
    "$ISO_ROOT/L/fastfilesystem" \
    "$ISO_ROOT/OS-Version3.9/Workbench3.5/L/FastFileSystem" \
    "$ISO_ROOT/OS-Version3.9/Workbench3.5/L/fastfilesystem" \
    "$ISO_ROOT/OS-Version3.9/Workbench3.9/L/FastFileSystem" \
    "$ISO_ROOT/OS-Version3.9/Workbench3.9/L/fastfilesystem"; do
    if [[ -f "$ffs_candidate" ]]; then
      FFS_PATH="$ffs_candidate"
      break
    fi
  done
fi

if [[ -n "$FFS_PATH" ]]; then
  echo "==> FastFileSystem source: $FFS_PATH"
fi

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

# -----------------------------------------------------------------------------
# 1) Create blank image (Windows-aligned)
# -----------------------------------------------------------------------------
echo "==> 1) Create blank image"
mkdir -p "$(dirname "$IMG")"
if [[ -e "$IMG" ]]; then
  echo "==> Removing existing image: $IMG"
  rm -f "$IMG"
fi
run "$HST_IMAGER" blank "$IMG" "$DISK_SIZE_BYTES"

# -----------------------------------------------------------------------------
# 2) Format PiStorm preset (Windows-aligned)
# -----------------------------------------------------------------------------
echo "==> 2) Format PiStorm preset"
if [[ "$HST_FS" == "Pfs3" ]]; then
  run "$HST_IMAGER" format "$IMG" PiStorm Pfs3 --size "$IMAGE_SIZE" --file-system-path "$PFS3_AIO"
else
  run "$HST_IMAGER" format "$IMG" PiStorm "$HST_FS" --size "$IMAGE_SIZE" --file-system-path "$FFS_PATH"
fi

FAT_ROOT="$IMG/mbr/1"

# Best-effort: ensure FAT volume label is EMU68BOOT (some first-boot scripts refer to EMU68BOOT:)
if "$HST_IMAGER" mbr part format "$IMG/mbr" 1 EMU68BOOT >/dev/null 2>&1; then
  echo "==> FAT volume label set to EMU68BOOT"
else
  echo "==> NOTE: Could not set FAT volume label via mbr part format (tool may not support it)"
fi

RDB="$IMG/mbr/2"

if ! "$HST_IMAGER" fs dir "$FAT_ROOT" >/dev/null 2>&1; then
  die "PiStorm format failed: FAT partition not accessible at $FAT_ROOT"
fi
if ! "$HST_IMAGER" rdb info "$RDB" >/dev/null 2>&1; then
  die "PiStorm format failed: RDB slice not accessible at $RDB"
fi

# -----------------------------------------------------------------------------
# 3) Partition layout (N partitions, Windows-parity compatible)
# -----------------------------------------------------------------------------
echo "==> 3) Partition layout (${NUM_PARTS} partitions)"

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)"

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

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"

# Compute byte sizes per partition (handles "remaining" keyword)
PART_BYTES=()
_fixed_total=0
_remaining_idx=-1
for _i in $(seq 0 $((NUM_PARTS - 1))); do
  if [[ "${PART_SIZES[$_i]}" == "remaining" ]]; then
    _remaining_idx=$_i
    PART_BYTES+=("0")
  else
    _bytes=$(( ${PART_SIZES[$_i]} * 1024 * 1024 ))
    _fixed_total=$(( _fixed_total + _bytes ))
    PART_BYTES+=("$_bytes")
  fi
done

# Assign remaining space to the "remaining" partition
if [[ $_remaining_idx -ge 0 ]]; then
  _remain=$(( USABLE_BYTES - _fixed_total ))
  [[ $_remain -gt 0 ]] || _remain=0
  PART_BYTES[$_remaining_idx]=$_remain
fi

echo "==> RDB sizing: total=${RDB_BYTES}B usable=${USABLE_BYTES}B fixed=${_fixed_total}B partitions=${NUM_PARTS}"
for _i in $(seq 0 $((NUM_PARTS - 1))); do
  echo "    ${PART_NAMES[$_i]}: ${PART_BYTES[$_i]}B (requested: ${PART_SIZES[$_i]})"
done

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

RDB_INFO_CLEAN="$($HST_IMAGER rdb info "$RDB" --verbose 2>/dev/null || true)"
if echo "$RDB_INFO_CLEAN" | grep -qE '^\s*[0-9]+\s*\|'; then
  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
fi

# When PFS3 is the primary filesystem, force ALL partitions to PFS3.
# The RDB only has PFS3 registered (no FFS), so mixed types would fail with
# "File system with DOS type 'DOS3' not found in Rigid Disk Block".
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

# Create N partitions (Windows parity RDB params)
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 512 --max-transfer 0x1fe00 --mask 0x7ffffffe \
    --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

# Primary system partition path (used by downstream steps)
SYS_ROOT="$IMG/mbr/2/rdb/${PART_PNS[0]}"

# Resolve transfer target partition path (defaults to SYS_ROOT)
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="$IMG/mbr/2/rdb/${PART_PNS[$_transfer_idx]}"
  else
    echo "==> WARN: transfer partition '$TRANSFER_PARTITION' not found; using ${PART_NAMES[0]}" >&2
  fi
fi

# -----------------------------------------------------------------------------
# 4) Populate FAT (kick.rom + Emu68 boot bundle)
# -----------------------------------------------------------------------------
echo "==> 4) Populate FAT (kick.rom + Emu68 boot bundle)"

# IMPORTANT: Strip any kick.rom from the boot bundle BEFORE copying to FAT.
# The boot bundle often ships its own kick.rom (typically 3.1) and hst-imager
# cannot reliably overwrite an existing file on a FAT image partition.
# We stage the boot bundle to a temp dir, remove any kick.rom variants,
# then copy to FAT cleanly. The user's ROM is copied separately afterward.

BOOT_FROM_ASSETS=0
if [[ "$EMU68_BOOT_MODE" == "assets" ]]; then
  BOOT_FROM_ASSETS=1
elif [[ "$EMU68_BOOT_MODE" == "download" ]]; then
  BOOT_FROM_ASSETS=0
else
  assets_boot_bundle_is_complete && BOOT_FROM_ASSETS=1
fi

BOOT_STAGE="$STAGE/boot-bundle-clean"
rm -rf "$BOOT_STAGE" >/dev/null 2>&1 || true
mkdir -p "$BOOT_STAGE"

if [[ "$BOOT_FROM_ASSETS" -eq 1 ]]; then
  echo "==> FAT boot bundle: using Assets/AmigaFiles/EMU68Boot (looks complete)"
  cp -R "$ASSETS_DIR/AmigaFiles/EMU68Boot/"* "$BOOT_STAGE/" 2>/dev/null || true
  cp -R "$ASSETS_DIR/AmigaFiles/EMU68Boot/".* "$BOOT_STAGE/" 2>/dev/null || true
else
  echo "==> FAT boot bundle: downloading Emu68 PiStorm release zip (Assets boot bundle incomplete)"
  ZIP_URL="$(resolve_emu68_pistorm_zip_url)"
  echo "==> Emu68 zip: $ZIP_URL"

  EMU68_ZIP="$STAGE/emu68.zip"
  EMU68_DIR="$STAGE/emu68"
  mkdir -p "$EMU68_DIR"

  curl -fL --retry 3 --retry-delay 2 -o "$EMU68_ZIP" "$ZIP_URL"
  unzip -q "$EMU68_ZIP" -d "$EMU68_DIR"

  payload="$EMU68_DIR"
  top_dirs="$(find "$EMU68_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
  top_files="$(find "$EMU68_DIR" -mindepth 1 -maxdepth 1 -type f | wc -l | tr -d ' ')"
  if [[ "$top_dirs" -eq 1 && "$top_files" -eq 0 ]]; then
    payload="$(find "$EMU68_DIR" -mindepth 1 -maxdepth 1 -type d | head -n1)"
  fi

  echo "==> Staging Emu68 boot payload from: $payload"
  cp -R "$payload/"* "$BOOT_STAGE/" 2>/dev/null || true
  cp -R "$payload/".* "$BOOT_STAGE/" 2>/dev/null || true
fi

# Remove any kick.rom variants from the staged boot bundle (case-insensitive)
echo "==> Stripping bundled kick.rom from boot payload (user ROM will be installed separately)"
find "$BOOT_STAGE" -maxdepth 1 -iname "kick.rom" -type f -delete 2>/dev/null || true
find "$BOOT_STAGE" -maxdepth 1 -iname "kick*.rom" -type f -delete 2>/dev/null || true

# --- Use Assets config.txt template if available (has GPIO-based PiStorm detection) ---
# The Assets config.txt is a template with [ROMPATH] and [VIDEOMODES] placeholders
# that the Windows mja65 tool also uses. The downloaded Emu68 zip has a "sample"
# config.txt WITHOUT GPIO detection, which breaks PiStorm32Lite/PiStorm16 variants.
USED_ASSETS_CONFIG_TEMPLATE=0
ASSETS_CONFIG="$ASSETS_DIR/AmigaFiles/EMU68Boot/config.txt"
if [[ -f "$ASSETS_CONFIG" ]]; then
  echo "==> Using Assets config.txt template (has PiStorm GPIO detection)"
  USED_ASSETS_CONFIG_TEMPLATE=1
  cp -f "$ASSETS_CONFIG" "$BOOT_STAGE/config.txt"

  # Replace [ROMPATH] placeholder with initramfs kick.rom
  perl -pi -e 's/^\[ROMPATH\]$/initramfs kick.rom/' "$BOOT_STAGE/config.txt"

  # Replace [VIDEOMODES] placeholder with screen mode settings
  VIDMODE_BLOCK=""
  if [[ -n "$SCREEN_WIDTH" && -n "$SCREEN_HEIGHT" ]]; then
    # Custom screen mode from Phase 2 settings
    VIDMODE_BLOCK="hdmi_group=2
hdmi_mode=87
hdmi_cvt=${SCREEN_WIDTH} ${SCREEN_HEIGHT} ${SCREEN_FPS:-60}
disable_overscan=1"
  else
    # Automatic screen mode (same as Windows default)
    VIDMODE_BLOCK="hdmi_group=0
disable_overscan=1"
  fi
  perl -pi -e "s/^\\[VIDEOMODES\\]\$/${VIDMODE_BLOCK}/" "$BOOT_STAGE/config.txt"

  # Also copy ps32lite firmware from Assets if present
  if [[ -f "$ASSETS_DIR/AmigaFiles/EMU68Boot/ps32lite-stealth-firmware.gz" ]]; then
    cp -f "$ASSETS_DIR/AmigaFiles/EMU68Boot/ps32lite-stealth-firmware.gz" "$BOOT_STAGE/"
  fi
else
  echo "==> NOTE: No Assets config.txt template found; using boot bundle's config.txt"
fi

# Ensure kernel names referenced by config are present in the boot payload.
ensure_boot_bundle_kernel_aliases "$BOOT_STAGE"

ls -la "$BOOT_STAGE/" >&2 || true

# Copy the cleaned boot bundle to FAT
echo "==> Copying cleaned boot payload to FAT"
fs_copy "$BOOT_STAGE/" "$FAT_ROOT/" --recursive --makedir

# Now copy the user's Kickstart ROM — no bundled kick.rom can exist on FAT at this point
echo "==> FAT: Installing user Kickstart ROM as kick.rom ($(basename "$KICK_ROM"))"
fs_copy "$KICK_ROM" "$FAT_ROOT/kick.rom"

ensure_emu68_config_initramfs_points_to_kick "$FAT_ROOT"
ensure_emu68_cmdline_files "$FAT_ROOT"

# --- ToolTypes modification helpers (Windows parity) --------------------------

# Check if hst-amiga is available
hst_amiga_available() {
  [[ -n "${HST_AMIGA:-}" && -x "${HST_AMIGA:-}" ]] && return 0
  # Also check the default path
  [[ -x "$DEFAULT_HST_AMIGA" ]] && { HST_AMIGA="$DEFAULT_HST_AMIGA"; return 0; }
  return 1
}

# export_tooltypes()
#   Export .info file tooltypes to a text file using hst-amiga.
#   $1 = info_path   (path to .info file, can be host or image path)
#   $2 = output_file (host path for exported tooltypes)
export_tooltypes() {
  local info_path="$1"
  local output_file="$2"
  hst_amiga_available || return 1
  "$HST_AMIGA" icon tooltypes export "$info_path" "$output_file" 2>/dev/null
}

# import_tooltypes()
#   Import tooltypes from a text file into an .info file using hst-amiga.
#   $1 = info_path   (path to .info file)
#   $2 = input_file  (host path with tooltype lines)
import_tooltypes() {
  local info_path="$1"
  local input_file="$2"
  hst_amiga_available || return 1
  "$HST_AMIGA" icon tooltypes import "$info_path" "$input_file" 2>/dev/null
}

# modify_tooltype()
#   High-level: export tooltypes, modify/add a key=value, import back.
#   $1 = info_path   (path to .info file)
#   $2 = tt_name     (tooltype name, e.g. "DONOTWAIT")
#   $3 = tt_value    (value, or empty for flag-style tooltypes)
modify_tooltype() {
  local info_path="$1"
  local tt_name="$2"
  local tt_value="${3:-}"

  if ! hst_amiga_available; then
    echo "==> WARN: hst-amiga not available; cannot modify tooltype $tt_name for $info_path"
    return 0
  fi

  local tmp_tt="$STAGE/tt-work.txt"

  if ! export_tooltypes "$info_path" "$tmp_tt"; then
    echo "==> WARN: Failed to export tooltypes from $info_path"
    return 1
  fi

  local line="$tt_name"
  [[ -n "$tt_value" ]] && line="${tt_name}=${tt_value}"

  if grep -qi "^${tt_name}" "$tmp_tt" 2>/dev/null; then
    # Replace existing
    perl -pi -e "s/^${tt_name}.*/${line}/i" "$tmp_tt"
  else
    # Append new
    printf "%s\n" "$line" >> "$tmp_tt"
  fi

  if ! import_tooltypes "$info_path" "$tmp_tt"; then
    echo "==> WARN: Failed to import tooltypes for $info_path"
    return 1
  fi

  echo "==> Modified tooltype: $info_path -> $line"
  rm -f "$tmp_tt" 2>/dev/null || true
}

# replace_tooltype_line()
#   Replace an exact tooltype line with a new value.  Handles parenthesized
#   (disabled) ToolTypes that modify_tooltype() can't match by key prefix.
#   $1 = info_path   (path to .info file, can be host or image path)
#   $2 = old_line     (exact line to find, e.g. "(FORCEOPENXAD)")
#   $3 = new_line     (replacement line, e.g. "FORCEOPENXAD")
replace_tooltype_line() {
  local info_path="$1"
  local old_line="$2"
  local new_line="$3"

  if ! hst_amiga_available; then
    echo "==> WARN: hst-amiga not available; cannot replace tooltype '$old_line' for $info_path"
    return 0
  fi

  local tmp_tt="$STAGE/tt-replace.txt"

  if ! export_tooltypes "$info_path" "$tmp_tt"; then
    echo "==> WARN: Failed to export tooltypes from $info_path"
    return 1
  fi

  if grep -qF "$old_line" "$tmp_tt" 2>/dev/null; then
    sed -i '' "s|$(printf '%s' "$old_line" | sed 's|[&/\]|\\&|g')|${new_line}|g" "$tmp_tt"
    if import_tooltypes "$info_path" "$tmp_tt"; then
      echo "==> Replaced tooltype: $info_path -> '$old_line' => '$new_line'"
    else
      echo "==> WARN: Failed to import tooltypes for $info_path"
    fi
  else
    echo "==> Tooltype '$old_line' not found in $info_path (skipping)"
  fi

  rm -f "$tmp_tt" 2>/dev/null || true
}

# apply_dopus_tooltype_fix()
#   Enable the FORCEOPENXAD ToolType in DirectoryOpus.info (Windows parity).
#   $1 = sys_root
apply_dopus_tooltype_fix() {
  local sys_root="$1"
  local info_img="$sys_root/Programs/DOpus/DirectoryOpus.info"
  local tmp_info="$STAGE/DirectoryOpus.info"

  if ! hst_amiga_available; then
    echo "==> DOpus ToolType fix: skipped (hst-amiga not available)"
    return 0
  fi

  if ! fs_pull_file "$info_img" "$tmp_info" 2>/dev/null; then
    echo "==> DOpus ToolType fix: DirectoryOpus.info not found in image (skipping)"
    return 0
  fi

  replace_tooltype_line "$tmp_info" "(FORCEOPENXAD)" "FORCEOPENXAD"
  fs_copy_file "$tmp_info" "$info_img"
  rm -f "$tmp_info" 2>/dev/null || true
}

# download_and_install_clicon()
#   Download CLICon from Aminet and install to SYS:System/.
#   CLICon (Thomas Richter) is an IconX replacement needed by PiStorm tools
#   (OnlineWifi, OnlineGENet, Offline) which use CLICon ToolTypes for script execution.
#   $1 = sys_root
download_and_install_clicon() {
  local sys_root="$1"

  # Skip if CLICon already exists on image (e.g. from HD install media)
  if "$HST_IMAGER" fs dir "$sys_root/System/CLICon" >/dev/null 2>&1; then
    echo "==> CLICon: already present in System/"
    return 0
  fi

  echo "==> CLICon: downloading from Aminet (needed by PiStorm tools)"
  local clicon_url="https://aminet.net/util/wb/CLICon.lha"
  local clicon_lha="$STAGE/CLICon.lha"

  if ! curl -fL --connect-timeout 10 --max-time 30 --retry 2 --retry-delay 2 \
       -o "$clicon_lha" "$clicon_url" 2>/dev/null; then
    echo "==> WARN: CLICon download failed; PiStorm Online/Offline tools will not work"
    return 0
  fi

  local tmp_cc="$STAGE/clicon-extract"
  rm -rf "$tmp_cc" 2>/dev/null || true
  mkdir -p "$tmp_cc"

  "$HST_IMAGER" fs extract "$clicon_lha" "$tmp_cc" --recursive --force 2>/dev/null || \
  "$HST_IMAGER" fs extract "$clicon_lha" "$tmp_cc" --recursive 2>/dev/null || {
    echo "==> WARN: Failed to extract CLICon LHA"
    rm -rf "$tmp_cc" 2>/dev/null || true
    return 0
  }

  local clicon_bin=""
  clicon_bin="$(find "$tmp_cc" -type f -name "CLICon" 2>/dev/null | head -n1)"
  if [[ -n "$clicon_bin" && -f "$clicon_bin" ]]; then
    ensure_img_dir "$sys_root/System"
    fs_copy_file "$clicon_bin" "$sys_root/System/CLICon"
    echo "==> CLICon installed to System/CLICon"
  else
    echo "==> WARN: CLICon binary not found in extracted archive"
  fi

  rm -rf "$tmp_cc" "$clicon_lha" 2>/dev/null || true
}

# download_and_install_mecho()
#   Download mecho from Aminet and install to SYS:C/.
#   mecho (Michael Glicksman) is an enhanced echo command supporting escape sequences
#   (e.g. \r for carriage return). Required by S:ProgressBar for the network progress
#   animation used by PiStorm Online/Offline tools.
#   $1 = sys_root
download_and_install_mecho() {
  local sys_root="$1"

  # Skip if mecho already exists on image
  if "$HST_IMAGER" fs dir "$sys_root/C/mecho" >/dev/null 2>&1; then
    echo "==> mecho: already present in C/"
    return 0
  fi

  echo "==> mecho: downloading from Aminet (needed by S:ProgressBar)"
  local mecho_url="https://aminet.net/util/batch/mecho.lha"
  local mecho_lha="$STAGE/mecho.lha"

  if ! curl -fL --connect-timeout 10 --max-time 30 --retry 2 --retry-delay 2 \
       -o "$mecho_lha" "$mecho_url" 2>/dev/null; then
    echo "==> WARN: mecho download failed; PiStorm progress bar will not work"
    return 0
  fi

  local tmp_me="$STAGE/mecho-extract"
  rm -rf "$tmp_me" 2>/dev/null || true
  mkdir -p "$tmp_me"

  "$HST_IMAGER" fs extract "$mecho_lha" "$tmp_me" --recursive --force 2>/dev/null || \
  "$HST_IMAGER" fs extract "$mecho_lha" "$tmp_me" --recursive 2>/dev/null || {
    echo "==> WARN: Failed to extract mecho LHA"
    rm -rf "$tmp_me" 2>/dev/null || true
    return 0
  }

  local mecho_bin=""
  mecho_bin="$(find "$tmp_me" -type f -name "mecho" 2>/dev/null | head -n1)"
  if [[ -z "$mecho_bin" ]]; then
    # Some archives capitalise the name
    mecho_bin="$(find "$tmp_me" -type f -iname "mecho" 2>/dev/null | head -n1)"
  fi
  if [[ -n "$mecho_bin" && -f "$mecho_bin" ]]; then
    local tmp_mecho_bin="$STAGE/mecho.$$.$RANDOM"
    cp -f "$mecho_bin" "$tmp_mecho_bin"
    write_uaem_with_perms "----rwed" "$tmp_mecho_bin"
    ensure_img_dir "$sys_root/C"
    fs_copy_file "$tmp_mecho_bin" "$sys_root/C/mecho" --uaemetadata UaeMetafile
    echo "==> mecho installed to C/mecho"
  else
    echo "==> WARN: mecho binary not found in extracted archive"
  fi

  rm -rf "$tmp_me" "$mecho_lha" 2>/dev/null || true
}

# download_and_install_wirelessmanager()
#   Download prism2v2.lha from Aminet and install WirelessManager + wpa_passphrase
#   to SYS:C/. WirelessManager is required by the PiStorm Online script for WiFi:
#   it associates with the WiFi AP using ENVARC:Sys/Wireless.prefs before
#   AddNetInterface can bring the wifipi interface up.
#   $1 = sys_root
download_and_install_wirelessmanager() {
  local sys_root="$1"

  # Skip if wirelessmanager already exists on image
  if "$HST_IMAGER" fs dir "$sys_root/C/wirelessmanager" >/dev/null 2>&1 || \
     "$HST_IMAGER" fs dir "$sys_root/C/WirelessManager" >/dev/null 2>&1; then
    echo "==> WirelessManager: already present in C/"
    return 0
  fi

  echo "==> WirelessManager: downloading prism2v2 (needed for WiFi)"
  local p2_url="http://ftp2.grandis.nu/turran/FTP/Misc/WHDUpdate/datafile/Amiga/Prism2v2.lha"
  local p2_url_backup="https://aminet.net/driver/net/prism2v2.lha"
  local p2_lha="$STAGE/prism2v2.lha"
  if ! curl -fL --connect-timeout 10 --max-time 60 --retry 2 --retry-delay 2 \
       -o "$p2_lha" "$p2_url" 2>/dev/null; then
    echo "==> WirelessManager: primary URL failed, trying Aminet fallback"
    if ! curl -fL --connect-timeout 10 --max-time 60 --retry 2 --retry-delay 2 \
         -o "$p2_lha" "$p2_url_backup" 2>/dev/null; then
      echo "==> WARN: prism2v2 download failed; WiFi connection will not work"
      return 0
    fi
  fi

  local tmp_p2="$STAGE/prism2v2-extract"
  rm -rf "$tmp_p2" 2>/dev/null || true
  mkdir -p "$tmp_p2"

  "$HST_IMAGER" fs extract "$p2_lha" "$tmp_p2" --recursive --force 2>/dev/null || \
  "$HST_IMAGER" fs extract "$p2_lha" "$tmp_p2" --recursive 2>/dev/null || {
    echo "==> WARN: Failed to extract prism2v2 LHA"
    rm -rf "$tmp_p2" 2>/dev/null || true
    return 0
  }

  ensure_img_dir "$sys_root/C"

  # Install WirelessManager
  local wm_bin=""
  wm_bin="$(find "$tmp_p2" -type f -iname 'WirelessManager' 2>/dev/null | head -n1)"
  if [[ -n "$wm_bin" && -f "$wm_bin" ]]; then
    local tmp_wm="$STAGE/wirelessmanager.$$.$RANDOM"
    cp -f "$wm_bin" "$tmp_wm"
    write_uaem_with_perms "----rwed" "$tmp_wm"
    fs_copy_file "$tmp_wm" "$sys_root/C/wirelessmanager" --uaemetadata UaeMetafile
    echo "==> WirelessManager installed to C/wirelessmanager"
  else
    echo "==> WARN: WirelessManager binary not found in prism2v2 archive"
  fi

  # Install wpa_passphrase (used by Wifi Config tool)
  local wp_bin=""
  wp_bin="$(find "$tmp_p2" -type f -iname 'wpa_passphrase' 2>/dev/null | head -n1)"
  if [[ -n "$wp_bin" && -f "$wp_bin" ]]; then
    local tmp_wp="$STAGE/wpa_passphrase.$$.$RANDOM"
    cp -f "$wp_bin" "$tmp_wp"
    write_uaem_with_perms "----rwed" "$tmp_wp"
    fs_copy_file "$tmp_wp" "$sys_root/C/wpa_passphrase" --uaemetadata UaeMetafile
    echo "==> wpa_passphrase installed to C/wpa_passphrase"
  fi

  rm -rf "$tmp_p2" "$p2_lha" 2>/dev/null || true
}

# download_and_install_screennotify()
#   Download ScreenNotify 1.0 from Aminet and install screennotify.library to LIBS:.
#   Required by WBDock2 to detect screen mode changes.
#   $1 = sys_root
download_and_install_screennotify() {
  local sys_root="$1"

  if "$HST_IMAGER" fs dir "$sys_root/Libs/screennotify.library" >/dev/null 2>&1; then
    echo "==> screennotify.library: already present in LIBS:"
    return 0
  fi

  echo "==> screennotify.library: downloading from Aminet (needed by WBDock2)"
  local sn_url="https://aminet.net/util/libs/ScreenNotify10.lha"
  local sn_lha="$STAGE/ScreenNotify10.lha"

  if ! curl -fL --connect-timeout 10 --max-time 30 --retry 2 --retry-delay 2 \
       -o "$sn_lha" "$sn_url" 2>/dev/null; then
    echo "==> WARN: screennotify.library download failed"
    return 0
  fi

  local tmp_sn="$STAGE/screennotify-extract"
  rm -rf "$tmp_sn" 2>/dev/null || true
  mkdir -p "$tmp_sn"

  local _lha_bin=""
  for _l in lha /usr/local/bin/lha /opt/homebrew/bin/lha; do
    command -v "$_l" >/dev/null 2>&1 && _lha_bin="$_l" && break
    [[ -x "$_l" ]] && _lha_bin="$_l" && break
  done
  local _sn_ok=0
  if [[ -n "$_lha_bin" ]]; then
    (cd "$tmp_sn" && "$_lha_bin" x "$sn_lha") && _sn_ok=1
  fi
  if (( _sn_ok == 0 )); then
    "$HST_IMAGER" fs extract "$sn_lha" "$tmp_sn" --recursive --force 2>/dev/null || \
    "$HST_IMAGER" fs extract "$sn_lha" "$tmp_sn" --recursive 2>/dev/null || {
      echo "==> WARN: Failed to extract ScreenNotify LHA"
      rm -rf "$tmp_sn" 2>/dev/null || true
      return 0
    }
  fi

  local sn_lib=""
  sn_lib="$(find "$tmp_sn" -type f -iname 'screennotify.library' 2>/dev/null | head -n1)"
  if [[ -n "$sn_lib" && -f "$sn_lib" ]]; then
    ensure_img_dir "$sys_root/Libs"
    fs_copy_file "$sn_lib" "$sys_root/Libs/screennotify.library"
    echo "==> screennotify.library installed to LIBS:"
  else
    echo "==> WARN: screennotify.library not found in extracted archive"
  fi

  rm -rf "$tmp_sn" "$sn_lha" 2>/dev/null || true
}

# download_and_install_iconlib()
#   Download IconLib 46.4 from Aminet and install icon.library (68020+) to LIBS:.
#   Enhanced icon.library with support for GlowIcons, PNG, TrueColor formats.
#   $1 = sys_root
download_and_install_iconlib() {
  local sys_root="$1"

  echo "==> icon.library 46.4: downloading from Aminet"
  local il_url="https://aminet.net/util/libs/IconLib_46.4.lha"
  local il_lha="$STAGE/IconLib_46.4.lha"

  if ! curl -fL --connect-timeout 10 --max-time 60 --retry 2 --retry-delay 2 \
       -o "$il_lha" "$il_url" 2>/dev/null; then
    echo "==> WARN: icon.library download failed"
    return 0
  fi

  local tmp_il="$STAGE/iconlib-extract"
  rm -rf "$tmp_il" 2>/dev/null || true
  mkdir -p "$tmp_il"

  local _lha_bin=""
  for _l in lha /usr/local/bin/lha /opt/homebrew/bin/lha; do
    command -v "$_l" >/dev/null 2>&1 && _lha_bin="$_l" && break
    [[ -x "$_l" ]] && _lha_bin="$_l" && break
  done
  local _il_ok=0
  if [[ -n "$_lha_bin" ]]; then
    (cd "$tmp_il" && "$_lha_bin" x "$il_lha") && _il_ok=1
  fi
  if (( _il_ok == 0 )); then
    "$HST_IMAGER" fs extract "$il_lha" "$tmp_il" --recursive --force 2>/dev/null || \
    "$HST_IMAGER" fs extract "$il_lha" "$tmp_il" --recursive 2>/dev/null || {
      echo "==> WARN: Failed to extract IconLib LHA"
      rm -rf "$tmp_il" 2>/dev/null || true
      return 0
    }
  fi

  # Use the 68020+ version (Emu68 emulates 68040)
  local il_lib=""
  il_lib="$(find "$tmp_il" -path '*/Libs/icon.library' ! -path '*/68000/*' ! -path '*/Bonus/*' 2>/dev/null | head -n1)"
  if [[ -n "$il_lib" && -f "$il_lib" ]]; then
    ensure_img_dir "$sys_root/Libs"
    fs_copy_file "$il_lib" "$sys_root/Libs/icon.library"
    echo "==> icon.library 46.4 (68020+) installed to LIBS:"
  else
    echo "==> WARN: icon.library not found in extracted archive"
  fi

  rm -rf "$tmp_il" "$il_lha" 2>/dev/null || true
}

# download_and_install_whdload_wrapper()
#   Download the latest WHDLoadWrapper from grandis.nu and install into the
#   image.  This replaces the original WHDLoad binary (already renamed to
#   WHDLoad.ori by the CSV installer) with the wrapper that auto-selects the
#   correct kickstart ROM per game.
#   $1 = sys_root  (image path to SYS: volume)
download_and_install_whdload_wrapper() {
  local sys_root="$1"
  echo "==> WHDLoadWrapper: downloading and installing"

  # --- 1) Discover the latest version URL from grandis.nu search ----------
  local search_url="http://ftp2.grandis.nu/turransearch/search.php?_search_=1&search=WHDLoadWrapper&category_id=&exclude=&limit=10&httplinks=on&username=ftp%2Cany&filesonly=on"
  local search_html="$STAGE/whdwrapper-search.html"
  local wrapper_url=""

  if curl -fsSL --connect-timeout 10 --max-time 30 -o "$search_html" "$search_url" 2>/dev/null; then
    # Extract WHDLoadWrapper LHA URLs, pick the highest version number
    wrapper_url="$(grep -oE 'http[s]?://[^"'"'"' <>]+WHDLoadWrapper[0-9]+\.lha' "$search_html" 2>/dev/null \
      | sort -t 'r' -k2 -V | tail -n1 || true)"
  fi
  rm -f "$search_html" 2>/dev/null || true

  # Fallback to last known version if search failed
  if [[ -z "$wrapper_url" ]]; then
    wrapper_url="http://ftp2.grandis.nu/turran/FTP/Misc/WHDUpdate/datafile/WHDLoadWrapper017.lha"
    echo "  -> Using fallback URL: $wrapper_url"
  else
    echo "  -> Found latest: $wrapper_url"
  fi

  # --- 2) Download the LHA ------------------------------------------------
  local wrapper_lha="$STAGE/WHDLoadWrapper.lha"
  if ! curl -fL --connect-timeout 10 --max-time 60 --retry 2 --retry-delay 2 \
       -o "$wrapper_lha" "$wrapper_url" 2>/dev/null; then
    echo "==> WARN: WHDLoadWrapper download failed; WHDLoad.ori will remain without wrapper"
    return 0
  fi

  # --- 3) Extract ----------------------------------------------------------
  local tmp_ww="$STAGE/whdwrapper-extract"
  rm -rf "$tmp_ww" 2>/dev/null || true
  mkdir -p "$tmp_ww"

  "$HST_IMAGER" fs extract "$wrapper_lha" "$tmp_ww" --recursive --force 2>/dev/null || \
  "$HST_IMAGER" fs extract "$wrapper_lha" "$tmp_ww" --recursive 2>/dev/null || {
    echo "==> WARN: Failed to extract WHDLoadWrapper LHA"
    rm -rf "$tmp_ww" 2>/dev/null || true
    return 0
  }

  # --- 4) Install C/ binaries to SYS:C/ -----------------------------------
  ensure_img_dir "$sys_root/C"
  local c_dir=""
  c_dir="$(find "$tmp_ww" -maxdepth 2 -type d -name "C" | head -n1 || true)"
  if [[ -n "$c_dir" && -d "$c_dir" ]]; then
    local f
    for f in "$c_dir"/*; do
      [[ -f "$f" ]] || continue
      local fname
      fname="$(basename "$f")"
      fs_copy_file "$f" "$sys_root/C/$fname"
      echo "  -> C/$fname -> SYS:C/$fname"
    done

    # Also install WHDLoadWrapper as SYS:C/WHDLoad (the replacement binary)
    if [[ -f "$c_dir/WHDLoadWrapper" ]]; then
      fs_copy_file "$c_dir/WHDLoadWrapper" "$sys_root/C/WHDLoad"
      echo "  -> C/WHDLoadWrapper -> SYS:C/WHDLoad (wrapper replaces original)"
    fi
  fi

  # --- 5) Install S/WHDLoad-Wrapper/ tree (scripts, Updater/, etc.) -------
  ensure_img_dir "$sys_root/S/WHDLoad-Wrapper"
  local s_dir=""
  s_dir="$(find "$tmp_ww" -maxdepth 3 -type d -name "WHDLoad-Wrapper" -path "*/S/*" | head -n1 || true)"
  if [[ -n "$s_dir" && -d "$s_dir" ]]; then
    # Install files directly in WHDLoad-Wrapper/
    local f
    for f in "$s_dir"/*; do
      [[ -f "$f" ]] || continue
      local fname
      fname="$(basename "$f")"
      # Rename StartupScript_ScreenText -> StartupScript (Windows parity)
      if [[ "$fname" == "StartupScript_ScreenText" ]]; then
        fs_copy_file "$f" "$sys_root/S/WHDLoad-Wrapper/StartupScript"
        echo "  -> S/WHDLoad-Wrapper/StartupScript_ScreenText -> StartupScript"
      else
        fs_copy_file "$f" "$sys_root/S/WHDLoad-Wrapper/$fname"
        echo "  -> S/WHDLoad-Wrapper/$fname"
      fi
    done
    # Recursively install subdirectories (e.g. Updater/, containing WHDUpdate)
    local subdir
    for subdir in "$s_dir"/*/; do
      [[ -d "$subdir" ]] || continue
      local subname
      subname="$(basename "$subdir")"
      echo "  -> S/WHDLoad-Wrapper/$subname/ (recursive)"
      fs_copy_tree_filtered "$subdir" "$sys_root/S/WHDLoad-Wrapper/$subname"
    done
  fi

  # --- 6) Install DB/ and DB-General/ database files ----------------------
  local db_dir
  for db_dir in $(find "$tmp_ww" -maxdepth 2 -type d \( -name "DB" -o -name "DB-General" \) 2>/dev/null); do
    local f
    for f in "$db_dir"/*; do
      [[ -f "$f" ]] || continue
      local fname
      fname="$(basename "$f")"
      fs_copy_file "$f" "$sys_root/S/WHDLoad-Wrapper/$fname"
      echo "  -> $(basename "$db_dir")/$fname -> S/WHDLoad-Wrapper/$fname"
    done
  done

  rm -rf "$tmp_ww" 2>/dev/null || true
  echo "==> WHDLoadWrapper: installation complete"
}

# install_whdload_kickstarts()
#   Scan a user-supplied folder for Kickstart ROM files, identify them by MD5
#   hash, and copy recognized ROMs to DEVS:Kickstarts/ with the canonical
#   WHDLoad filenames (e.g. kick40068.A1200).
#   $1 = sys_root               (image path to SYS: volume)
#   $2 = kickstarts_source_dir  (host path to folder with ROM files)
install_whdload_kickstarts() {
  local sys_root="$1"
  local src_dir="$2"
  echo "==> WHDLoad Kickstarts: scanning $src_dir for recognized ROMs"

  # Locate the hash file (bundled in SYS:S/ or from Assets)
  local hash_file=""
  hash_file="$(find "$ASSETS_DIR" -name "KickStartRomHashes" -o -name "KickstartRomHashes" 2>/dev/null | head -n1 || true)"
  if [[ -z "$hash_file" || ! -f "$hash_file" ]]; then
    echo "==> WARN: KickStartRomHashes not found in assets; skipping kickstart ROM installation"
    return 0
  fi

  ensure_img_dir "$sys_root/Devs/Kickstarts"

  local installed=0
  local f
  for f in "$src_dir"/*; do
    [[ -f "$f" ]] || continue

    local fsize
    fsize="$(stat -f '%z' "$f" 2>/dev/null || stat -c '%s' "$f" 2>/dev/null || echo 0)"
    # Accept 256KB (262144) and 512KB (524288) ROMs — both are valid kickstart sizes
    if [[ "$fsize" -ne 524288 && "$fsize" -ne 262144 ]]; then
      continue
    fi

    local md5
    md5="$(md5 -q "$f" 2>/dev/null || md5sum "$f" 2>/dev/null | awk '{print $1}' || true)"
    [[ -n "$md5" ]] || continue

    # Case-insensitive hash lookup in the hash file (format: Hash;WHDLoadName)
    local match_line
    match_line="$(grep -i "^${md5};" "$hash_file" 2>/dev/null | head -n1 || true)"
    if [[ -n "$match_line" ]]; then
      local whdname
      whdname="$(echo "$match_line" | cut -d';' -f2 | tr -d '\r')"
      if [[ -n "$whdname" ]]; then
        fs_copy_file "$f" "$sys_root/Devs/Kickstarts/$whdname"
        echo "  -> $(basename "$f") => DEVS:Kickstarts/$whdname (MD5: $md5)"
        installed=$((installed + 1))

        # Copy companion .RTB and .PAT files — WHDLoad needs these for
        # ROM transfer backup data and runtime patching.
        local src_base
        src_base="$(basename "$f")"
        for ext in RTB PAT; do
          # By source filename (e.g., kick34005.A500.RTB alongside kick34005.A500)
          if [[ -f "$src_dir/${src_base}.${ext}" ]]; then
            fs_copy_file "$src_dir/${src_base}.${ext}" "$sys_root/Devs/Kickstarts/${whdname}.${ext}"
            echo "  -> ${src_base}.${ext} => DEVS:Kickstarts/${whdname}.${ext}"
          # By canonical WHDLoad name (if source file was renamed)
          elif [[ -f "$src_dir/${whdname}.${ext}" ]]; then
            fs_copy_file "$src_dir/${whdname}.${ext}" "$sys_root/Devs/Kickstarts/${whdname}.${ext}"
            echo "  -> ${whdname}.${ext} => DEVS:Kickstarts/${whdname}.${ext}"
          fi
        done
      fi
    fi
  done

  # Also install the hash file itself to SYS:S/ for runtime reference
  fs_copy_file "$hash_file" "$sys_root/S/KickStartRomHashes"

  if [[ "$installed" -gt 0 ]]; then
    echo "==> WHDLoad Kickstarts: installed $installed ROM(s)"
  else
    echo "==> WHDLoad Kickstarts: no recognized ROMs found in $src_dir"
  fi
}

# --- CSV and path helpers (Windows parity data model) ------------------------

trim_spaces() {
  local s="${1:-}"
  s="${s#"${s%%[![:space:]]*}"}"
  s="${s%"${s##*[![:space:]]}"}"
  printf "%s" "$s"
}

normalize_rel_path() {
  local p="${1:-}"
  p="${p//\\//}"
  p="$(trim_spaces "$p")"
  p="${p#./}"
  while [[ "$p" == /* ]]; do
    p="${p#/}"
  done
  while [[ "$p" == *"//"* ]]; do
    p="${p//\/\//\/}"
  done
  # Remove trailing slashes
  while [[ "$p" == */ ]]; do
    p="${p%/}"
  done
  printf "%s" "$p"
}

img_dir_has_entry() {
  # Check if an exact entry name exists in an image directory listing.
  # $1 = image directory path
  # $2 = exact entry name
  local dir_path="$1"
  local entry_name="$2"
  local listing
  listing="$("$HST_IMAGER" fs dir "$dir_path" 2>/dev/null || true)"
  [[ -n "$listing" ]] || return 1

  printf "%s\n" "$listing" | awk -F'|' -v want="$entry_name" '
    function trim(s){ sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
    /^[[:space:]]*Name[[:space:]]*\|/ { next }
    /^[[:space:]]*[-]+[[:space:]]*\|/ { next }
    /^[[:space:]]*Path:/ { next }
    /^[[:space:]]*Entries:/ { next }
    /^[[:space:]]*$/ { next }
    /\[CMD\]/ { next }
    /\[INF\]/ { next }
    {
      name=trim($1)
      if (name == want) { found=1; exit 0 }
    }
    END { if (!found) exit 1 }
  '
}

img_dir_has_entry_ci() {
  # Case-insensitive entry check for Amiga drawers/files.
  # Use this for command presence checks where case is irrelevant (e.g. C:lha).
  local dir_path="$1"
  local entry_name="$2"
  local listing
  listing="$("$HST_IMAGER" fs dir "$dir_path" 2>/dev/null || true)"
  [[ -n "$listing" ]] || return 1

  printf "%s\n" "$listing" | awk -F'|' -v want="$entry_name" '
    function trim(s){ sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
    BEGIN { want_l=tolower(want) }
    /^[[:space:]]*Name[[:space:]]*\|/ { next }
    /^[[:space:]]*[-]+[[:space:]]*\|/ { next }
    /^[[:space:]]*Path:/ { next }
    /^[[:space:]]*Entries:/ { next }
    /^[[:space:]]*$/ { next }
    /\[CMD\]/ { next }
    /\[INF\]/ { next }
    {
      name=tolower(trim($1))
      if (name == want_l) { found=1; exit 0 }
    }
    END { if (!found) exit 1 }
  '
}

find_input_csv() {
  local filename="${1:-}"
  local base="${filename%.*}"
  local ext="${filename##*.}"
  local lower="${base}.$(echo "$ext" | tr '[:upper:]' '[:lower:]')"
  local cand

  # If --csv-dir was passed, check it first (single source of truth).
  if [[ -n "$CSV_DIR" ]]; then
    for cand in "$CSV_DIR/$filename" "$CSV_DIR/$lower"; do
      [[ -f "$cand" ]] && { echo "$cand"; return 0; }
    done
  fi

  for cand in \
    "$ASSETS_DIR/../InputFiles/$filename" \
    "$ASSETS_DIR/../InputFiles/$lower" \
    "$ASSETS_DIR/InputFiles/$filename" \
    "$ASSETS_DIR/InputFiles/$lower" \
    "$ASSETS_DIR/AmigaFiles/InputFiles/$filename" \
    "$ASSETS_DIR/AmigaFiles/InputFiles/$lower" \
    "$ASSETS_DIR/$filename" \
    "$ASSETS_DIR/$lower"
  do
    [[ -f "$cand" ]] && { echo "$cand"; return 0; }
  done
  return 1
}

icon_type_to_num() {
  local raw
  raw="$(trim_spaces "${1:-}")"
  [[ -z "$raw" ]] && { echo ""; return 0; }
  local low
  low="$(echo "$raw" | tr '[:upper:]' '[:lower:]')"
  case "$low" in
    1|disk) echo "1" ;;
    2|drawer) echo "2" ;;
    3|tool) echo "3" ;;
    4|project) echo "4" ;;
    5|garbage) echo "5" ;;
    6|device) echo "6" ;;
    7|kick) echo "7" ;;
    8|appicon) echo "8" ;;
    *) echo "" ;;
  esac
}

resolve_drive_root() {
  local drive
  drive="$(trim_spaces "${1:-}")"
  [[ -z "$drive" ]] && return 1

  local d_upper
  d_upper="$(echo "$drive" | tr '[:lower:]' '[:upper:]')"

  case "$d_upper" in
    SYS|SYSTEM|DH0|SDH0|WORKBENCH)
      echo "$SYS_ROOT"
      return 0
      ;;
    SD0|EMU68BOOT|BOOT|FAT)
      echo "$FAT_ROOT"
      return 0
      ;;
  esac

  local _pi part_name part_label
  for _pi in $(seq 0 $((NUM_PARTS - 1))); do
    part_name="$(echo "${PART_NAMES[$_pi]:-}" | tr '[:lower:]' '[:upper:]')"
    part_label="$(echo "${PART_LABELS[$_pi]:-}" | tr '[:lower:]' '[:upper:]')"
    if [[ "$d_upper" == "$part_name" || "$d_upper" == "$part_label" || "$d_upper" == "DH${_pi}" ]]; then
      if [[ "$_pi" -eq 0 ]]; then
        echo "$SYS_ROOT"
      else
        echo "$IMG/mbr/2/rdb/${PART_PNS[$_pi]}"
      fi
      return 0
    fi
  done

  return 1
}

hst_icon_update_in_place() {
  local info_path="$1"
  local icon_type="${2:-}"
  local current_x="${3:-}"
  local current_y="${4:-}"
  local drawer_x="${5:-}"
  local drawer_y="${6:-}"
  local drawer_width="${7:-}"
  local drawer_height="${8:-}"

  hst_amiga_available || return 1
  [[ -f "$info_path" ]] || return 1

  local cmd=( "$HST_AMIGA" icon update "$info_path" )
  [[ -n "$icon_type" ]] && cmd+=( --type "$icon_type" )

  local x_upper y_upper
  x_upper="$(echo "$current_x" | tr '[:lower:]' '[:upper:]')"
  y_upper="$(echo "$current_y" | tr '[:lower:]' '[:upper:]')"

  [[ -n "$current_x" && "$x_upper" != "FREEX" ]] && cmd+=( --current-x "$current_x" )
  [[ -n "$current_y" && "$y_upper" != "FREEY" ]] && cmd+=( --current-y "$current_y" )
  [[ -n "$drawer_x" ]] && cmd+=( --drawer-x "$drawer_x" )
  [[ -n "$drawer_y" ]] && cmd+=( --drawer-y "$drawer_y" )
  [[ -n "$drawer_width" ]] && cmd+=( --drawer-width "$drawer_width" )
  [[ -n "$drawer_height" ]] && cmd+=( --drawer-height "$drawer_height" )

  "${cmd[@]}" >/dev/null 2>&1
}

apply_icon_update_to_image_file() {
  local img_info="$1"
  local icon_type="${2:-}"
  local current_x="${3:-}"
  local current_y="${4:-}"
  local drawer_x="${5:-}"
  local drawer_y="${6:-}"
  local drawer_width="${7:-}"
  local drawer_height="${8:-}"

  local tmp_info="$STAGE/hst-icon-update-$(basename "$img_info").$$.$RANDOM.info"
  if ! fs_pull_file "$img_info" "$tmp_info"; then
    return 1
  fi

  if ! hst_icon_update_in_place "$tmp_info" "$icon_type" "$current_x" "$current_y" "$drawer_x" "$drawer_y" "$drawer_width" "$drawer_height"; then
    rm -f "$tmp_info" 2>/dev/null || true
    return 1
  fi

  fs_copy_file "$tmp_info" "$img_info"
  rm -f "$tmp_info" 2>/dev/null || true
  return 0
}

resolve_tooltype_revised_file() {
  local rel="${1:-}"
  rel="$(normalize_rel_path "$rel")"
  [[ -z "$rel" ]] && return 1

  if [[ -f "$rel" ]]; then
    echo "$rel"
    return 0
  fi

  local cand
  for cand in \
    "$ASSETS_DIR/AmigaFiles/System/$rel" \
    "$ASSETS_DIR/AmigaFiles/$rel" \
    "$ASSETS_DIR/$rel"
  do
    if [[ -f "$cand" ]]; then
      echo "$cand"
      return 0
    fi
  done

  return 1
}

apply_tooltype_overrides_in_place() {
  local info_path="$1"
  local mode_raw="$2"      # modify | replace
  local revised_file="$3"  # host path with revised tooltypes

  hst_amiga_available || return 1
  [[ -f "$info_path" ]] || return 1
  [[ -f "$revised_file" ]] || return 1

  local mode
  mode="$(echo "$(trim_spaces "$mode_raw")" | tr '[:upper:]' '[:lower:]')"
  [[ "$mode" == "replace" ]] || mode="modify"

  local tmp_new="$STAGE/tooltypes-new.$$.$RANDOM"
  local tmp_existing="$STAGE/tooltypes-existing.$$.$RANDOM"
  local tmp_merged="$STAGE/tooltypes-merged.$$.$RANDOM"

  if head -n1 "$revised_file" | grep -q ';'; then
    awk -F';' '
      function trim(s){ gsub(/\r/, "", s); sub(/^[ \t"]+/, "", s); sub(/[ \t"]+$/, "", s); return s }
      function norm(s){ s=tolower(trim(s)); gsub(/[^a-z0-9]/, "", s); return s }
      NR==1{
        for(i=1;i<=NF;i++) h[norm($i)] = i
        if (!(h["newvalue"] || h["newtooltype"] || h["tooltype"] || h["value"])) {
          v = trim($1)
          if (v != "") print v
        }
        next
      }
      {
        v = ""
        if (h["newvalue"])      v = trim($(h["newvalue"]))
        else if (h["newtooltype"]) v = trim($(h["newtooltype"]))
        else if (h["value"])    v = trim($(h["value"]))
        else if (h["tooltype"]) v = trim($(h["tooltype"]))
        else                    v = trim($1)
        if (v != "") print v
      }
    ' "$revised_file" > "$tmp_new"
  else
    awk '
      { gsub(/\r/, "", $0) }
      /^[[:space:]]*$/ { next }
      /^[[:space:]]*[#;]/ { next }
      { print }
    ' "$revised_file" > "$tmp_new"
  fi

  if [[ ! -s "$tmp_new" ]]; then
    rm -f "$tmp_new" "$tmp_existing" "$tmp_merged" 2>/dev/null || true
    return 1
  fi

  if [[ "$mode" == "replace" ]]; then
    cp "$tmp_new" "$tmp_merged"
  else
    if ! export_tooltypes "$info_path" "$tmp_existing"; then
      : > "$tmp_existing"
    fi
    cp "$tmp_existing" "$tmp_merged"

    while IFS= read -r newline || [[ -n "$newline" ]]; do
      newline="$(trim_spaces "$(printf "%s" "$newline" | tr -d '\r')")"
      [[ -z "$newline" ]] && continue
      local key
      key="$(trim_spaces "${newline%%=*}")"
      if [[ -n "$key" ]]; then
        awk -v key="$key" '
          BEGIN { IGNORECASE=1 }
          {
            raw=$0
            gsub(/\r/, "", raw)
            t=raw
            sub(/^[ \t]+/, "", t)
            split(t, a, "=")
            if (toupper(a[1]) == toupper(key)) next
            print raw
          }
        ' "$tmp_merged" > "$tmp_merged.next"
        mv "$tmp_merged.next" "$tmp_merged"
      fi
      printf "%s\n" "$newline" >> "$tmp_merged"
    done < "$tmp_new"
  fi

  make_lf_only "$tmp_merged"
  if ! import_tooltypes "$info_path" "$tmp_merged"; then
    rm -f "$tmp_new" "$tmp_existing" "$tmp_merged" 2>/dev/null || true
    return 1
  fi

  rm -f "$tmp_new" "$tmp_existing" "$tmp_merged" 2>/dev/null || true
  return 0
}

apply_tooltypes_from_package_csv() {
  if ! hst_amiga_available; then
    echo "==> Tooltypes: hst-amiga not available (skipping CSV-driven tooltype updates)"
    return 0
  fi

  local csv_path
  csv_path="$(find_input_csv "ListofPackagestoInstall.CSV" || true)"
  if [[ -z "$csv_path" ]]; then
    echo "==> Tooltypes: ListofPackagestoInstall.CSV not found (skipping)"
    return 0
  fi

  local rows="$STAGE/tooltype-rows.tsv"
  awk -F';' '
    function trim(s){ gsub(/\r/, "", s); sub(/^[ \t"]+/, "", s); sub(/[ \t"]+$/, "", s); return s }
    function norm(s){ s=tolower(trim(s)); gsub(/[^a-z0-9]/, "", s); return s }
    function get(k,   idx,val){ idx=h[k]; if (idx == 0) return ""; val=trim($(idx)); return val }
    NR==1{
      for(i=1;i<=NF;i++) h[norm($i)] = i
      next
    }
    {
      drive=get("drivetoinstall")
      loc=get("locationtoinstall")
      src=get("filestoinstall")
      newf=get("newfilename")
      mtype=get("modifyinfofiletype")
      mtt=get("modifyinfofiletooltype")
      rev=get("pathtorevisedtooltypeinfo")
      if (tolower(mtype) == "false") mtype=""
      if (tolower(mtt) == "false") mtt=""
      if (mtype == "" && mtt == "") next
      # Use pipe delimiter - shell IFS collapses consecutive whitespace but preserves non-whitespace delimiters
      print drive "|" loc "|" src "|" newf "|" mtype "|" mtt "|" rev
    }
  ' "$csv_path" > "$rows"

  local total=0 updated=0 skipped=0
  while IFS='|' read -r drive loc src_file new_name mtype mtt rev; do
    [[ -z "$drive$loc$src_file$new_name$mtype$mtt$rev" ]] && continue
    total=$((total + 1))

    local root
    root="$(resolve_drive_root "$drive" || true)"
    if [[ -z "$root" ]]; then
      skipped=$((skipped + 1))
      continue
    fi

    local icon_name
    icon_name="$(trim_spaces "$new_name")"
    if [[ -z "$icon_name" ]]; then
      icon_name="$(basename "$(trim_spaces "$src_file")")"
    fi
    [[ -z "$icon_name" ]] && { skipped=$((skipped + 1)); continue; }

    local loc_norm
    loc_norm="$(normalize_rel_path "$loc")"
    local img_info="$root"
    [[ -n "$loc_norm" ]] && img_info="$img_info/$loc_norm"
    img_info="$img_info/$icon_name"

    local tmp_info="$STAGE/tooltype-update-$(basename "$icon_name").$$.$RANDOM.info"
    if ! fs_pull_file "$img_info" "$tmp_info"; then
      skipped=$((skipped + 1))
      continue
    fi

    local changed=0
    local type_num
    type_num="$(icon_type_to_num "$mtype")"
    if [[ -n "$type_num" ]]; then
      if hst_icon_update_in_place "$tmp_info" "$type_num"; then
        changed=1
      fi
    fi

    local mode_lower revised_path
    mode_lower="$(echo "$(trim_spaces "$mtt")" | tr '[:upper:]' '[:lower:]')"
    if [[ -n "$mode_lower" && "$mode_lower" != "false" ]]; then
      revised_path="$(resolve_tooltype_revised_file "$rev" || true)"
      if [[ -n "$revised_path" ]]; then
        if apply_tooltype_overrides_in_place "$tmp_info" "$mode_lower" "$revised_path"; then
          changed=1
        fi
      fi
    fi

    if [[ "$changed" -eq 1 ]]; then
      fs_copy_file "$tmp_info" "$img_info"
      updated=$((updated + 1))
    else
      skipped=$((skipped + 1))
    fi

    rm -f "$tmp_info" 2>/dev/null || true
  done < "$rows"

  echo "==> Tooltypes: processed=$total updated=$updated skipped=$skipped (from $(basename "$csv_path"))"
}

apply_icon_positions_from_csv() {
  if ! hst_amiga_available; then
    echo "==> Icon positions: hst-amiga not available (skipping CSV-driven updates)"
    return 0
  fi

  local csv_path
  csv_path="$(find_input_csv "IconPositions.CSV" || true)"
  if [[ -z "$csv_path" ]]; then
    echo "==> Icon positions: IconPositions.CSV not found (skipping)"
    return 0
  fi

  local rows="$STAGE/iconpos-rows.tsv"
  awk -F';' -v wb_ver="$DETECTED_WB_VERSION" '
    function trim(s){ gsub(/\r/, "", s); sub(/^[ \t"]+/, "", s); sub(/[ \t"]+$/, "", s); return s }
    function norm(s){ s=tolower(trim(s)); gsub(/[^a-z0-9]/, "", s); return s }
    function get(k,   idx,val){ idx=h[k]; if (idx == 0) return ""; val=trim($(idx)); return val }
    # Map sub-version to major family: 3.2.2.1 -> 3.2, 3.1.4 -> 3.1
    function ver_family(v,  parts,n){
      n = split(v, parts, ".")
      if (n >= 2) return parts[1] "." parts[2]
      return v
    }
    # Check if wb_ver matches any version in a comma-separated list
    # Matches: exact, CSV family == wb_ver, CSV value == wb_ver family, both families equal
    function ver_matches(csv_versions,  n,arr,i,v,wf){
      wf = ver_family(wb_ver)
      n = split(csv_versions, arr, ",")
      for (i = 1; i <= n; i++) {
        v = trim(arr[i])
        if (v == wb_ver) return 1
        if (v == wf) return 1
        if (ver_family(v) == wf) return 1
      }
      return 0
    }
    NR==1{
      for(i=1;i<=NF;i++) h[norm($i)] = i
      next
    }
    {
      # Include filter: skip rows where Include is explicitly FALSE
      inc = get("include")
      if (inc != "" && toupper(inc) != "TRUE") next

      # KickstartVersion filter: skip rows that specify a version not matching ours
      kv = get("kickstartversion")
      if (kv != "" && !ver_matches(kv)) next

      drive=get("drive")
      file=get("file")
      typ=get("type")
      x=get("iconx")
      y=get("icony")
      dx=get("drawerx")
      dy=get("drawery")
      dw=get("drawerwidth")
      dh=get("drawerheight")
      if (file == "") next
      # Use pipe delimiter - shell IFS collapses consecutive whitespace but preserves non-whitespace delimiters
      print drive "|" file "|" typ "|" x "|" y "|" dx "|" dy "|" dw "|" dh
    }
  ' "$csv_path" > "$rows"

  local matched_rows
  matched_rows="$(wc -l < "$rows" | tr -d ' ')"
  echo "==> Icon positions: $matched_rows rows matched for WB version $DETECTED_WB_VERSION"

  local total=0 updated=0 skipped=0
  while IFS='|' read -r drive file_raw type_raw x y dx dy dw dh; do
    [[ -z "$file_raw" ]] && continue
    total=$((total + 1))

    local rel_path drive_from_file root
    rel_path="$file_raw"
    if [[ "$rel_path" == *:* ]]; then
      drive_from_file="${rel_path%%:*}"
      rel_path="${rel_path#*:}"
      if [[ -z "$(trim_spaces "$drive")" ]]; then
        drive="$drive_from_file"
      fi
    fi

    root="$(resolve_drive_root "$drive" || true)"
    if [[ -z "$root" ]]; then
      skipped=$((skipped + 1))
      continue
    fi

    rel_path="$(normalize_rel_path "$rel_path")"
    [[ -z "$rel_path" ]] && { skipped=$((skipped + 1)); continue; }

    local type_num
    type_num="$(icon_type_to_num "$type_raw")"

    if [[ -z "$type_num" && -z "$x" && -z "$y" && -z "$dx" && -z "$dy" && -z "$dw" && -z "$dh" ]]; then
      skipped=$((skipped + 1))
      continue
    fi

    # Amiga icon files are stored as <name>.info — append if not already present
    local info_path="$root/$rel_path"
    [[ "$info_path" != *.info ]] && info_path="${info_path}.info"

    if apply_icon_update_to_image_file "$info_path" "$type_num" "$x" "$y" "$dx" "$dy" "$dw" "$dh"; then
      updated=$((updated + 1))
    else
      skipped=$((skipped + 1))
    fi
  done < "$rows"

  echo "==> Icon positions: processed=$total updated=$updated skipped=$skipped (from $(basename "$csv_path"))"
}

# --- Roadshow install (Windows parity) ---------------------------------------

# copy_roadshow_s_dir()
#   Copy Roadshow Workbench/S content but skip User-Startup and Network-Startup
#   (we manage those ourselves).
#   $1 = source S directory on host
#   $2 = destination S directory in image
copy_roadshow_s_dir() {
  local src_dir="$1"
  local dst_dir="$2"

  [[ -d "$src_dir" ]] || return 0
  ensure_img_dir "$dst_dir"

  # Create destination directories first.
  while IFS= read -r rel; do
    rel="${rel#./}"
    case "$(basename "$rel")" in
      __MACOSX) continue ;;
    esac
    ensure_img_dir "$dst_dir/$rel"
  done < <(cd "$src_dir" && find . -type d -mindepth 1 -print | LC_ALL=C sort)

  # Copy files with filtering.
  (cd "$src_dir" && find . -type f -mindepth 1 -print0) | while IFS= read -r -d '' rel; do
    rel="${rel#./}"
    if is_skippable_overlay_file "$rel"; then
      continue
    fi
    case "$(basename "$rel")" in
      User-Startup|Network-Startup) continue ;;
    esac
    fs_copy_file "$src_dir/$rel" "$dst_dir/$rel"
  done
}

# extract_roadshow_bsdsocket()
#   When Roadshow network stack is selected, find the Roadshow LHA archive,
#   install core Roadshow files into SYS:, and extract bsdsocket.library.
#   Searches for demo or full version depending on ROADSHOW_VARIANT.
#   $1 = sys_root
extract_roadshow_bsdsocket() {
  local sys_root="$1"

  [[ "${NETWORK_STACK:-}" == "roadshow" ]] || return 0

  local variant="${ROADSHOW_VARIANT:-full}"

  # Search for Roadshow LHA in multiple locations
  local roadshow_lha=""

  # 0) Explicit Roadshow archive path (preferred)
  if [[ -n "${ROADSHOW_ARCHIVE:-}" ]]; then
    if [[ -f "$ROADSHOW_ARCHIVE" ]]; then
      roadshow_lha="$ROADSHOW_ARCHIVE"
    else
      echo "==> WARN: Roadshow archive not found at: $ROADSHOW_ARCHIVE"
    fi
  fi

  # 1) Aminet staging dir
  if [[ -z "$roadshow_lha" && -n "${AMINET_DIR:-}" && -d "${AMINET_DIR:-}" ]]; then
    if [[ "$variant" == "demo" ]]; then
      for cand in "$AMINET_DIR"/RoadshowDemo*.lha "$AMINET_DIR"/Roadshow-Demo*.lha "$AMINET_DIR"/roadshowdemo*.lha; do
        [[ -f "$cand" ]] && roadshow_lha="$cand" && break
      done
    fi
    if [[ -z "$roadshow_lha" ]]; then
      for cand in "$AMINET_DIR"/Roadshow*.lha "$AMINET_DIR"/roadshow*.lha; do
        [[ -f "$cand" ]] && roadshow_lha="$cand" && break
      done
    fi
  fi

  # 2) Assets / LocalAmigaPackages (skip in download-only mode)
  if [[ -z "$roadshow_lha" && "$DOWNLOAD_ONLY" != "1" ]]; then
    if [[ "$variant" == "demo" ]]; then
      for cand in \
        "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/"RoadshowDemo*.lha \
        "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/"Roadshow-Demo*.lha \
        "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/"roadshowdemo*.lha \
        "$ASSETS_DIR/AmigaFiles/Packages/Roadshow/"RoadshowDemo*.lha \
        "$ASSETS_DIR/AmigaFiles/Packages/Roadshow/"Roadshow-Demo*.lha; do
        [[ -f "$cand" ]] && roadshow_lha="$cand" && break
      done
    fi
    if [[ -z "$roadshow_lha" ]]; then
      for cand in \
        "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/"Roadshow*.lha \
        "$ASSETS_DIR/AmigaFiles/Packages/Roadshow/"Roadshow*.lha \
        "$ASSETS_DIR/AmigaFiles/Packages/Roadshow/"roadshow*.lha; do
        [[ -f "$cand" ]] && roadshow_lha="$cand" && break
      done
    fi
  fi

  if [[ -z "$roadshow_lha" ]]; then
    echo "==> WARN: Roadshow ($variant) selected but LHA archive not found; bsdsocket.library not extracted"
    echo "==> Provide Roadshow.lha (or RoadshowDemo.lha) via the app or Assets/LocalAmigaPackages/"
    return 0
  fi

  echo "==> Using Roadshow variant: $variant"

  echo "==> Extracting Roadshow from: $(basename "$roadshow_lha")"
  local tmp_rs="$STAGE/roadshow-extract"
  rm -rf "$tmp_rs" 2>/dev/null || true
  mkdir -p "$tmp_rs"

  # Extract using hst-imager (works on LHA archives)
  "$HST_IMAGER" fs extract "$roadshow_lha" "$tmp_rs" --recursive --force 2>/dev/null || \
  "$HST_IMAGER" fs extract "$roadshow_lha" "$tmp_rs" --recursive 2>/dev/null || {
    echo "==> WARN: Failed to extract Roadshow LHA with hst-imager"
    # Try system lha if available
    if command -v lha >/dev/null 2>&1; then
      (cd "$tmp_rs" && lha x "$roadshow_lha") 2>/dev/null || true
    fi
  }

  # If the archive is a container, pick the most appropriate nested LHA.
  local content_lha=""
  local lhas=()
  while IFS= read -r -d '' f; do
    lhas+=("$f")
  done < <(find "$tmp_rs" -maxdepth 1 -type f -iname "*.lha" -print0 2>/dev/null)

  if [[ "${#lhas[@]}" -gt 0 ]]; then
    if [[ "$variant" == "demo" ]]; then
      for f in "${lhas[@]}"; do
        if echo "$f" | grep -qi "demo"; then content_lha="$f"; break; fi
      done
    fi
    if [[ -z "$content_lha" ]]; then
      for f in "${lhas[@]}"; do
        if echo "$f" | grep -qi "roadshow" && ! echo "$f" | grep -qi "update\\|compact\\|demo"; then
          content_lha="$f"; break
        fi
      done
    fi
    [[ -z "$content_lha" ]] && content_lha="${lhas[0]}"
  fi

  local tmp_main="$tmp_rs"
  if [[ -n "$content_lha" ]]; then
    tmp_main="$STAGE/roadshow-main"
    rm -rf "$tmp_main" 2>/dev/null || true
    mkdir -p "$tmp_main"
    "$HST_IMAGER" fs extract "$content_lha" "$tmp_main" --recursive --force 2>/dev/null || \
    "$HST_IMAGER" fs extract "$content_lha" "$tmp_main" --recursive 2>/dev/null || true
  fi

  # Locate Workbench root inside the extracted archive.
  local wb_dir root
  wb_dir="$(find "$tmp_main" -type d -name "Workbench" -print -quit 2>/dev/null || true)"
  if [[ -n "$wb_dir" ]]; then
    root="${wb_dir%/Workbench}"

    # Copy Roadshow Workbench components into SYS:
    if [[ -d "$root/Workbench/C" ]]; then
      fs_copy_tree_filtered "$root/Workbench/C" "$sys_root/C"
    fi
    if [[ -d "$root/Workbench/Libs" ]]; then
      fs_copy_tree_filtered "$root/Workbench/Libs" "$sys_root/Libs"
    fi
    if [[ -d "$root/Workbench/Devs" ]]; then
      fs_copy_tree_filtered "$root/Workbench/Devs" "$sys_root/Devs"
    fi
    if [[ -d "$root/Workbench/Locale" ]]; then
      fs_copy_tree_filtered "$root/Workbench/Locale" "$sys_root/Locale"
    fi
    if [[ -d "$root/Workbench/Storage" ]]; then
      fs_copy_tree_filtered "$root/Workbench/Storage" "$sys_root/Storage"
    fi
    if [[ -d "$root/Workbench/S" ]]; then
      copy_roadshow_s_dir "$root/Workbench/S" "$sys_root/S"
    fi

    # Prefer 68020+ bsdsocket.library when available.
    local bsd_lib=""
    if [[ -d "$root/Workbench/Libs/020" ]]; then
      bsd_lib="$(find "$root/Workbench/Libs/020" -name "bsdsocket.library" -print -quit 2>/dev/null || true)"
    fi
    if [[ -z "$bsd_lib" ]]; then
      bsd_lib="$(find "$root/Workbench/Libs" -maxdepth 1 -name "bsdsocket.library" -print -quit 2>/dev/null || true)"
    fi
    if [[ -n "$bsd_lib" ]]; then
      ensure_img_dir "$sys_root/Libs"
      fs_copy_file "$bsd_lib" "$sys_root/Libs/bsdsocket.library"
      echo "==> Installed bsdsocket.library to SYS:Libs/"
    else
      echo "==> WARN: bsdsocket.library not found in Roadshow archive"
    fi
  else
    echo "==> WARN: Roadshow Workbench folder not found after extraction"
  fi

  rm -rf "$tmp_rs" "$tmp_main" 2>/dev/null || true
}

# stage_p96_archive()
#   Extract Picasso96 archive at build time — iComp paid archive or Aminet
#   shareware fallback. Installs core libs, monitor drivers, tools, and prefs.
#   Files are placed directly into the image — no first-boot extraction needed.
#   $1 = sys_root
stage_p96_icomp_archive() {
  local sys_root="$1"
  local archive="${P96_ICOMP_ARCHIVE:-}"

  # Fallback chain: iComp archive → Aminet staging dir → download from Aminet
  if [[ -z "$archive" || ! -f "$archive" ]]; then
    if [[ -n "$archive" && ! -f "$archive" ]]; then
      echo "==> WARN: P96 iComp archive not found at '$archive'"
    fi
    archive=""
    # 1) Search Aminet staging directory for picasso96.lha
    if [[ -n "${AMINET_DIR:-}" ]]; then
      for cand in "$AMINET_DIR"/Picasso96.lha "$AMINET_DIR"/picasso96.lha; do
        [[ -f "$cand" ]] && archive="$cand" && break
      done
    fi
    # 2) Download Aminet shareware directly (same source as PC reference imager)
    if [[ -z "$archive" || ! -f "$archive" ]]; then
      echo "==> No P96 archive found — downloading Aminet shareware (same as PC reference)..."
      local _dl_lha="$STAGE/p96-aminet-dl/Picasso96.lha"
      mkdir -p "$(dirname "$_dl_lha")"
      if curl -fsSL --connect-timeout 15 --max-time 120 \
           -o "$_dl_lha" "http://aminet.net/driver/video/Picasso96.lha" 2>/dev/null; then
        archive="$_dl_lha"
        echo "==> Downloaded Aminet Picasso96.lha ($(wc -c < "$_dl_lha" | tr -d ' ') bytes)"
      else
        echo "==> WARN: Could not download Aminet P96 — RTG will not work"
        return 0
      fi
    fi
  fi

  local archive_name
  archive_name="$(basename "$archive")"
  echo "==> Extracting Picasso96 archive at build time: $archive_name"

  # Extract archive on host
  local tmp_p96="$STAGE/p96-extract"
  rm -rf "$tmp_p96" 2>/dev/null || true
  mkdir -p "$tmp_p96"

  # Try lha first (hst-imager has IndexOutOfRangeException on some LHA formats).
  # The Swift app launches with a minimal PATH, so check common locations.
  local _p96_extracted=0
  local _lha_bin=""
  for _l in lha /usr/local/bin/lha /opt/homebrew/bin/lha; do
    command -v "$_l" >/dev/null 2>&1 && _lha_bin="$_l" && break
    [[ -x "$_l" ]] && _lha_bin="$_l" && break
  done
  if [[ -n "$_lha_bin" ]]; then
    (cd "$tmp_p96" && "$_lha_bin" x "$archive") && _p96_extracted=1
  fi
  if (( _p96_extracted == 0 )); then
    "$HST_IMAGER" fs extract "$archive" "$tmp_p96" --recursive --force 2>/dev/null || \
    "$HST_IMAGER" fs extract "$archive" "$tmp_p96" --recursive 2>/dev/null || true
  fi

  # Locate the extracted root (Picasso96Install/ or similar)
  local p96_root=""
  p96_root="$(find "$tmp_p96" -maxdepth 1 -type d -name "Picasso96*" | head -n1)"
  if [[ -z "$p96_root" ]]; then
    echo "==> WARN: No Picasso96* directory found after extraction (skipping P96)"
    rm -rf "$tmp_p96" 2>/dev/null || true
    return 0
  fi
  echo "==> P96 archive root: $(basename "$p96_root")"

  # ---------------------------------------------------------------------------
  # Detect rtg.library version EARLY — determines which core P96 libs to use.
  #
  # Emu68-tools' VideoCore.card (v1.x) implements the Aminet shareware .card
  # API (rtg.library v40). It is INCOMPATIBLE with iComp P96 v41+ which uses
  # a completely different .card driver ABI. If the user's archive contains
  # v41+ rtg.library, installing it would produce zero RTG screen modes because
  # VideoCore.card can't register with the v41+ API.
  #
  # When v41+ is detected we download the Aminet shareware Picasso96.lha
  # (same as the PC reference imager) which contains rtg.library v40.3945,
  # emulation.library, Picasso96API.library, and the monitor binary.
  # Non-core components (P96Prefs, Picasso96Mode, etc.) still come from the
  # user's archive since they are version-independent.
  # ---------------------------------------------------------------------------
  local _rtg_lib_ver=0
  local _rtg_lib_path=""
  for _cand in "$p96_root/Libs/Picasso96/rtg.library" \
               "$p96_root/libs/Picasso96/rtg.library" \
               "$p96_root/libs/picasso96/rtg.library"; do
    [[ -f "$_cand" ]] && _rtg_lib_path="$_cand" && break
  done
  if [[ -n "$_rtg_lib_path" ]]; then
    _rtg_lib_ver="$(strings "$_rtg_lib_path" 2>/dev/null \
      | sed -n 's/.*rtg\.library \([0-9][0-9]*\)\..*/\1/p' | head -n1)"
    _rtg_lib_ver="${_rtg_lib_ver:-0}"
  fi

  local _p96_core_root="$p96_root"   # where to get core P96 libs/monitors
  local _need_legacy_id=1

  if (( _rtg_lib_ver >= 41 )); then
    echo "==> WARN: rtg.library v${_rtg_lib_ver} detected — INCOMPATIBLE with Emu68 VideoCore.card"
    echo "==>   VideoCore.card implements the Aminet shareware (v40) .card API."
    echo "==>   Downloading Aminet P96 v40 shareware for compatible core libraries..."
    _need_legacy_id=0   # default for v41+ (no VC4_LEGACY_ID); overridden if v40 fallback succeeds

    # Download the Aminet Picasso96.lha shareware — same source as the PC
    # reference imager. Contains rtg.library v40.3945 and all core P96 libs.
    local _sw_lha="$STAGE/p96-shareware-dl/Picasso96.lha"
    local _sw_extract="$STAGE/p96-shareware-extract"
    local _dl_ok=0
    mkdir -p "$(dirname "$_sw_lha")"
    rm -rf "$_sw_extract" 2>/dev/null || true
    mkdir -p "$_sw_extract"

    local _p96_aminet_url="http://aminet.net/driver/video/Picasso96.lha"
    echo "  -> Downloading Aminet Picasso96.lha..."
    if curl -fsSL --connect-timeout 15 --max-time 120 -o "$_sw_lha" "$_p96_aminet_url" 2>/dev/null; then
      _dl_ok=1
    fi

    if (( _dl_ok == 0 )); then
      echo "==> WARN: Could not download Aminet P96 shareware — RTG may not work"
      echo "==>   Falling back to provided archive (rtg.library v${_rtg_lib_ver})"
    else
      echo "  -> Extracting Aminet P96 shareware..."
      # hst-imager has a known IndexOutOfRangeException bug with this specific
      # LHA archive. Use lha first if available, then fall back to hst-imager.
      local _sw_extracted=0
      local _sw_lha_bin=""
      for _l in lha /usr/local/bin/lha /opt/homebrew/bin/lha; do
        command -v "$_l" >/dev/null 2>&1 && _sw_lha_bin="$_l" && break
        [[ -x "$_l" ]] && _sw_lha_bin="$_l" && break
      done
      if [[ -n "$_sw_lha_bin" ]]; then
        echo "  -> Using lha command ($_sw_lha_bin)..."
        (cd "$_sw_extract" && "$_sw_lha_bin" x "$_sw_lha") && _sw_extracted=1
      fi
      if (( _sw_extracted == 0 )); then
        echo "  -> Using hst-imager..."
        "$HST_IMAGER" fs extract "$_sw_lha" "$_sw_extract" --recursive --force 2>/dev/null || \
        "$HST_IMAGER" fs extract "$_sw_lha" "$_sw_extract" --recursive 2>/dev/null || true
      fi
      local _sw_root=""
      _sw_root="$(find "$_sw_extract" -maxdepth 1 -type d -name "Picasso96*" | head -n1)"
      if [[ -n "$_sw_root" && -d "$_sw_root" ]]; then
        # Verify rtg.library version
        local _sw_ver=0
        local _sw_rtg=""
        for _c in "$_sw_root/Libs/Picasso96/rtg.library" \
                  "$_sw_root/libs/Picasso96/rtg.library"; do
          [[ -f "$_c" ]] && _sw_rtg="$_c" && break
        done
        if [[ -n "$_sw_rtg" ]]; then
          _sw_ver="$(strings "$_sw_rtg" 2>/dev/null \
            | sed -n 's/.*rtg\.library \([0-9][0-9]*\)\..*/\1/p' | head -n1)"
          _sw_ver="${_sw_ver:-0}"
        fi
        if (( _sw_ver >= 41 )); then
          echo "==> WARN: Aminet P96 has rtg.library v${_sw_ver} — expected v40"
          echo "==>   Using provided archive anyway. RTG screen modes may not work."
          rm -rf "$_sw_extract" 2>/dev/null || true
        else
          echo "==> Aminet P96 shareware: rtg.library v${_sw_ver} — compatible with VideoCore.card"
          _p96_core_root="$_sw_root"
          _need_legacy_id=1   # shareware v40 needs VC4_LEGACY_ID
        fi
      else
        echo "==> WARN: Picasso96Install directory not found in Aminet archive"
      fi
    fi
  else
    echo "==> rtg.library v${_rtg_lib_ver:-unknown} — compatible with VideoCore.card (VC4_LEGACY_ID added)"
  fi

  # --- Libs/Picasso96API.library (from core source) ---
  if [[ -f "$_p96_core_root/Libs/Picasso96API.library" ]]; then
    ensure_img_dir "$sys_root/Libs"
    fs_copy_file "$_p96_core_root/Libs/Picasso96API.library" "$sys_root/Libs/Picasso96API.library"
  fi

  # --- Libs/Picasso96/ (rtg.library, emulation.library, .card + .chip drivers from core source) ---
  if [[ -d "$_p96_core_root/Libs/Picasso96" ]]; then
    ensure_img_dir "$sys_root/Libs/Picasso96"
    while IFS= read -r -d '' _f; do
      local _bn
      _bn="$(basename "$_f")"
      [[ "$_bn" == "_UAEFSDB.___" ]] && continue
      fs_copy_file "$_f" "$sys_root/Libs/Picasso96/$_bn"
    done < <(find "$_p96_core_root/Libs/Picasso96" -maxdepth 1 -type f -print0)
  fi

  # --- Devs/Monitors/ (from core source) ---
  if [[ -d "$_p96_core_root/devs/monitors" ]]; then
    ensure_img_dir "$sys_root/Devs/Monitors"
    for _f in "$_p96_core_root/devs/monitors"/*; do
      [[ -f "$_f" ]] || continue
      local _bn
      _bn="$(basename "$_f")"
      [[ "$_bn" == "_UAEFSDB.___" ]] && continue
      # Keep only generic monitor entries from the archive. We install
      # PiStorm/UAE aliases explicitly below with proper names/tooltypes.
      case "$_bn" in
        Picasso96|Picasso96.info) continue ;;
      esac
      fs_copy_file "$_f" "$sys_root/Devs/Monitors/$_bn"
    done
  fi
  # Startup-Sequence_UAEGFX expects legacy monitor names (Videocore/Uaegfx).
  # Keep Picasso96 as-is, but also create aliases for the UAEGFX swap logic.
  if [[ -f "$_p96_core_root/devs/monitors/Picasso96" ]]; then
    ensure_img_dir "$sys_root/Devs/Monitors"
    ensure_img_dir "$sys_root/Storage/Monitors"
    fs_copy_file "$_p96_core_root/devs/monitors/Picasso96" "$sys_root/Devs/Monitors/Videocore"
    fs_copy_file "$_p96_core_root/devs/monitors/Picasso96" "$sys_root/Storage/Monitors/Uaegfx"
  fi
  if [[ -f "$_p96_core_root/devs/monitors/Picasso96.info" ]]; then
    ensure_img_dir "$sys_root/Devs/Monitors"
    ensure_img_dir "$sys_root/Storage/Monitors"
    fs_copy_file "$_p96_core_root/devs/monitors/Picasso96.info" "$sys_root/Devs/Monitors/Videocore.info"
    fs_copy_file "$_p96_core_root/devs/monitors/Picasso96.info" "$sys_root/Storage/Monitors/Uaegfx.info"
  fi
  # Apply monitor tooltypes used by the Emu68 setup so P96 can build
  # graphics contexts and screenmodes without running the interactive installer.
  local vc_tooltypes_txt="$ASSETS_DIR/AmigaFiles/System/Devs/Monitors/Videocore.info.txt"
  if [[ -f "$vc_tooltypes_txt" ]]; then
    local _tmp_vc_info="$STAGE/p96-videocore.info"
    local _tmp_uae_info="$STAGE/p96-uaegfx.info"
    local _tmp_tt_raw="$STAGE/vc-tooltypes-raw.txt"
    local _tmp_tt_norm="$STAGE/vc-tooltypes-norm.txt"
    local _tmp_tt_nobt="$STAGE/vc-tooltypes-nobt.txt"
    local _tmp_vc_tt="$STAGE/vc-tooltypes.txt"
    local _tmp_uae_tt="$STAGE/uae-tooltypes.txt"

    # Extract tooltypes from the CSV-style file and normalize.
    awk -F';' '
      function trim(s){ gsub(/\r/, "", s); sub(/^[ \t"]+/, "", s); sub(/[ \t"]+$/, "", s); return s }
      function norm(s){ s=tolower(trim(s)); gsub(/[^a-z0-9]/, "", s); return s }
      NR==1{
        for(i=1;i<=NF;i++) h[norm($i)] = i
        next
      }
      {
        v = ""
        if (h["newvalue"])      v = trim($(h["newvalue"]))
        else if (h["newtooltype"]) v = trim($(h["newtooltype"]))
        else if (h["value"])    v = trim($(h["value"]))
        else if (h["tooltype"]) v = trim($(h["tooltype"]))
        else                    v = trim($1)
        if (v != "") print v
      }
    ' "$vc_tooltypes_txt" > "$_tmp_tt_raw"

    # Strip BOM/CR, drop blanks, and remove any existing BOARDTYPE lines.
    LC_ALL=C sed -e '1s/^\xEF\xBB\xBF//' -e 's/\r$//' "$_tmp_tt_raw" | awk 'NF' > "$_tmp_tt_norm"
    grep -vi '^BOARDTYPE=' "$_tmp_tt_norm" > "$_tmp_tt_nobt" || true

    # Strip VC4_LEGACY_ID when NOT needed (iComp/modern rtg.library v41+
    # that we couldn't replace with v40 shareware).
    if (( _need_legacy_id == 0 )); then
      grep -vi '^VC4_LEGACY_ID$' "$_tmp_tt_nobt" > "$_tmp_tt_nobt.tmp" || true
      mv -f "$_tmp_tt_nobt.tmp" "$_tmp_tt_nobt"
    fi

    printf "BOARDTYPE=VideoCore\n" > "$_tmp_vc_tt"
    cat "$_tmp_tt_nobt" >> "$_tmp_vc_tt"
    printf "BOARDTYPE=Uaegfx\n" > "$_tmp_uae_tt"
    cat "$_tmp_tt_nobt" >> "$_tmp_uae_tt"

    if fs_pull_file "$sys_root/Devs/Monitors/Videocore.info" "$_tmp_vc_info"; then
      if apply_tooltype_overrides_in_place "$_tmp_vc_info" "replace" "$_tmp_vc_tt"; then
        fs_copy_file "$_tmp_vc_info" "$sys_root/Devs/Monitors/Videocore.info"
      fi
    fi

    if fs_pull_file "$sys_root/Storage/Monitors/Uaegfx.info" "$_tmp_uae_info"; then
      if apply_tooltype_overrides_in_place "$_tmp_uae_info" "replace" "$_tmp_uae_tt"; then
        fs_copy_file "$_tmp_uae_info" "$sys_root/Storage/Monitors/Uaegfx.info"
      fi
    fi

    rm -f "$_tmp_vc_info" "$_tmp_uae_info" "$_tmp_tt_raw" "$_tmp_tt_norm" \
      "$_tmp_tt_nobt" "$_tmp_vc_tt" "$_tmp_uae_tt" 2>/dev/null || true
  fi
  # NOTE: Devs/Picasso96Settings is NOT copied from the archive — the Assets
  # overlay provides the custom Emu68 settings file (step 6d.1).

  # --- Programs/Picasso96/ (AttachSettings, FliP96, debug tools, etc.) ---
  if [[ -d "$p96_root/picasso96" ]]; then
    ensure_img_dir "$sys_root/Programs/Picasso96"
    fs_copy_tree_filtered "$p96_root/picasso96" "$sys_root/Programs/Picasso96"
  fi
  # Folder icon
  if [[ -f "$p96_root/Picasso96.info" ]]; then
    ensure_img_dir "$sys_root/Programs"
    fs_copy_file "$p96_root/Picasso96.info" "$sys_root/Programs/Picasso96.info"
  fi

  # --- Prefs (P96Prefs, Picasso96Mode) ---
  for _pref in P96Prefs P96Prefs.info Picasso96Mode Picasso96Mode.info; do
    if [[ -f "$p96_root/prefs/$_pref" ]]; then
      ensure_img_dir "$sys_root/Prefs"
      fs_copy_file "$p96_root/prefs/$_pref" "$sys_root/Prefs/$_pref"
    fi
  done

  # --- Commodities/P96ScreenCx ---
  for _cx in P96ScreenCx P96ScreenCx.info; do
    if [[ -f "$p96_root/Commodities/$_cx" ]]; then
      ensure_img_dir "$sys_root/Commodities"
      fs_copy_file "$p96_root/Commodities/$_cx" "$sys_root/Commodities/$_cx"
    fi
  done

  # --- Locale/Catalogs (P96Prefs translations) ---
  if [[ -d "$p96_root/Locale/Catalogs" ]]; then
    while IFS= read -r -d '' _lang_dir; do
      local _lang
      _lang="$(basename "$_lang_dir")"
      [[ "$_lang" == "_UAEFSDB.___" ]] && continue
      if [[ -f "$_lang_dir/P96Prefs.catalog" ]]; then
        ensure_img_dir "$sys_root/Locale/Catalogs/$_lang"
        fs_copy_file "$_lang_dir/P96Prefs.catalog" "$sys_root/Locale/Catalogs/$_lang/P96Prefs.catalog"
      fi
    done < <(find "$p96_root/Locale/Catalogs" -mindepth 1 -maxdepth 1 -type d -print0)
  fi

  echo "==> Picasso96 installed at build time (no first-boot extraction needed)"
  rm -rf "$tmp_p96" 2>/dev/null || true
}

# install_aminet_package_buildtime()
#   Extract an Aminet LHA and install files at build time using CSV mapping.
#   This matches the Windows Amiga Imager approach:
#     1. Extract LHA to temp folder
#     2. Copy matching files to INTERIM local folder (on host filesystem)
#     3. Copy entire interim folder to image in one operation
#   $1 = sys_root
#   $2 = lha_file (full path to the .lha file)
#   Returns 0 if package was installed, 1 if no CSV mapping found
install_aminet_package_buildtime() {
  local sys_root="$1"
  local lha_file="$2"

  local lha_base lha_stem lha_safe
  lha_base="$(basename "$lha_file")"
  lha_stem="${lha_base%.*}"
  lha_safe="$(printf '%s' "$lha_stem" | tr -c 'A-Za-z0-9._-' '_')"

  local csv_path
  csv_path="$(resolve_aminet_rules_csv || true)"
  [[ -n "$csv_path" ]] || return 1

  local archive_norm package_hint package_norm
  archive_norm="$(normalize_csv_token "$lha_base")"
  package_hint="$(aminet_package_hint_for_archive "$lha_base" || true)"
  package_norm="$(normalize_csv_token "$package_hint")"

  # Parse CSV to get rows for this package
  local rows="$STAGE/aminet-buildtime-$lha_safe.tsv"
  awk -F';' -v want_archive="$archive_norm" -v want_pkg="$package_norm" '
    function trim(s){ gsub(/\r/, "", s); sub(/^[ \t"]+/, "", s); sub(/[ \t"]+$/, "", s); return s }
    function norm(s){ s=tolower(trim(s)); gsub(/[^a-z0-9]/, "", s); return s }
    function get(k,   idx,val){ idx=h[k]; if (idx == 0) return ""; val=trim($(idx)); return val }
    NR==1{
      for(i=1;i<=NF;i++) h[norm($i)] = i
      next
    }
    {
      src_type=tolower(get("source"))
      if (src_type ~ /^web/) {
        dl=get("filedownloadname")
        pkg=get("packagename")
        dl_norm=norm(dl)
        pkg_norm=norm(pkg)
        do_match = 0
        if (dl_norm == want_archive) do_match = 1
        if (want_pkg == "") { } else { if (pkg_norm == want_pkg) do_match = 1 }
        if (do_match == 1) {
          drive=get("drivetoinstall")
          loc=get("locationtoinstall")
          src=get("filestoinstall")
          newf=get("newfilename")
          copyrec=get("copyrecursive")
          script_target=get("scriptbeingmodified")
          script_mode=get("modifyscript")
          script_changes=get("scriptpathtochanges")

          do_print = 0
          if (src == "") { } else { do_print = 1 }
          if (script_changes == "") { } else { do_print = 1 }
          if (do_print == 1) {
            print drive "|" loc "|" src "|" newf "|" copyrec "|" script_target "|" script_mode "|" script_changes
          }
        }
      }
    }
  ' "$csv_path" > "$rows"

  [[ -s "$rows" ]] || return 1

  # Extract LHA on host
  local tmp_extract="$STAGE/aminet-extract-$lha_safe"
  rm -rf "$tmp_extract" 2>/dev/null || true
  mkdir -p "$tmp_extract"

  if ! "$HST_IMAGER" fs extract "$lha_file" "$tmp_extract" --recursive --force 2>/dev/null; then
    if ! "$HST_IMAGER" fs extract "$lha_file" "$tmp_extract" --recursive 2>/dev/null; then
      echo "    WARN: Failed to extract $lha_base"
      rm -rf "$tmp_extract" 2>/dev/null || true
      return 1
    fi
  fi

  # Windows-style two-stage approach:
  # Stage 1: Copy matching files to INTERIM folder on host filesystem
  # Stage 2: Copy entire interim folder to image
  local tmp_interim="$STAGE/aminet-interim-$lha_safe"
  rm -rf "$tmp_interim" 2>/dev/null || true
  mkdir -p "$tmp_interim"

  local files_staged=0
  local drive loc src_file new_name copy_recursive script_target script_mode script_changes
  local script_changes_to_copy=()

  while IFS='|' read -r drive loc src_file new_name copy_recursive script_target script_mode script_changes; do
    # Track script modifications for later (even when no drive/source is specified).
    if [[ -n "$script_target" && -n "$script_changes" ]]; then
      script_changes_to_copy+=("$script_target|$script_mode|$script_changes")
    fi

    # Skip file staging if no source file is provided.
    local src_trim
    src_trim="$(trim_spaces "$src_file")"
    [[ -n "$src_trim" ]] || continue

    local drive_upper
    drive_upper="$(printf "%s" "$(trim_spaces "$drive")" | tr '[:lower:]' '[:upper:]')"

    # Only handle SYS: drive for now
    case "$drive_upper" in
      SYS|SYSTEM|DH0|SDH0|WORKBENCH) ;;
      *) continue ;;
    esac

    local loc_norm src_norm
    loc_norm="$(normalize_rel_path "$loc")"
    src_norm="$(normalize_rel_path "$src_file")"

    # Stage 1: Copy matching files to interim folder
    if [[ -n "$src_norm" ]]; then
      local interim_dest="$tmp_interim"
      [[ -n "$loc_norm" ]] && interim_dest="$tmp_interim/$loc_norm"

      # Create interim destination directory
      mkdir -p "$interim_dest"

      # Check if it's a wildcard pattern
      if [[ "$src_norm" == *"*"* ]]; then
        # Handle wildcards like "SysInfo/*"
        local glob_pattern="$src_norm"
        while IFS= read -r -d '' src_match; do
          local src_basename
          src_basename="$(basename "$src_match")"
          [[ "$src_basename" == "_UAEFSDB.___" ]] && continue
          [[ "$src_basename" == ".DS_Store" ]] && continue
          [[ "$src_basename" == "__uae___"* ]] && continue

          if [[ -d "$src_match" ]]; then
            # Directory matched by wildcard.
            # If the dir name equals the dest dir name (e.g. wildcard "Roadie*"
            # matches dir "Roadie" and dest is "Programs/Roadie"), merge contents
            # to avoid nesting (Programs/Roadie/Roadie/).
            # Otherwise copy as subdirectory (e.g. "MUI/*" matches "MUI/Libs"
            # and dest is "Programs/MUI" → keep as Programs/MUI/Libs/).
            local _dest_base
            _dest_base="$(basename "$interim_dest")"
            if [[ "$src_basename" == "$_dest_base" ]]; then
              mkdir -p "$interim_dest"
              for _sub in "$src_match"/*; do
                [[ -e "$_sub" ]] || continue
                local _sub_base
                _sub_base="$(basename "$_sub")"
                [[ "$_sub_base" == "_UAEFSDB.___" ]] && continue
                [[ "$_sub_base" == ".DS_Store" ]] && continue
                cp -R "$_sub" "$interim_dest/$_sub_base" 2>/dev/null || true
              done
            else
              cp -R "$src_match" "$interim_dest/$src_basename" 2>/dev/null || true
            fi
          elif [[ -f "$src_match" ]]; then
            # File - copy to interim
            cp "$src_match" "$interim_dest/$src_basename" 2>/dev/null || true
          fi
          files_staged=$((files_staged + 1))
        done < <(find "$tmp_extract" -path "$tmp_extract/$glob_pattern" -print0 2>/dev/null)
      else
        # Exact file path
        local src_path="$tmp_extract/$src_norm"
        if [[ -e "$src_path" ]]; then
          local dest_name
          if [[ -n "$(trim_spaces "$new_name")" ]]; then
            dest_name="$(normalize_rel_path "$new_name")"
            dest_name="${dest_name##*/}"
          else
            dest_name="$(basename "$src_norm")"
          fi

          if [[ -d "$src_path" ]]; then
            # Directory - copy recursively to interim
            cp -R "$src_path" "$interim_dest/$dest_name" 2>/dev/null || true
          else
            # File - copy to interim
            cp "$src_path" "$interim_dest/$dest_name" 2>/dev/null || true
          fi
          files_staged=$((files_staged + 1))
        fi
      fi
    fi

  done < "$rows"

  # Stage 2: Copy entire interim folder to image (Windows-style bulk copy)
  if [[ "$files_staged" -gt 0 ]]; then
    # Keep _UAEFSDB.___ — hst-imager reads these during fs copy to set Amiga protection bits
    find "$tmp_interim" -name ".DS_Store" -delete 2>/dev/null || true
    find "$tmp_interim" -name "__uae___*" -delete 2>/dev/null || true

    # Remove installer files - we're doing build-time installation so these aren't needed
    find "$tmp_interim" -type f -iname "Install" -delete 2>/dev/null || true
    find "$tmp_interim" -type f -iname "Install.info" -delete 2>/dev/null || true
    find "$tmp_interim" -type f -iname "Install *" -delete 2>/dev/null || true
    find "$tmp_interim" -type f -iname "Install *.info" -delete 2>/dev/null || true
    find "$tmp_interim" -type f -iname "*.installer" -delete 2>/dev/null || true
    find "$tmp_interim" -type f -iname "*.installer.info" -delete 2>/dev/null || true

    # Fix nested same-name directories (e.g. Programs/Roadie/Roadie/ → merge into
    # Programs/Roadie/). hst-imager has no fs delete, so we must fix this on the
    # host side before copying to the image.
    while IFS= read -r -d '' _nested_dir; do
      local _parent_dir _parent_name _nested_name
      _parent_dir="$(dirname "$_nested_dir")"
      _parent_name="$(basename "$_parent_dir")"
      _nested_name="$(basename "$_nested_dir")"
      if [[ "$_parent_name" == "$_nested_name" ]]; then
        # Merge contents up and remove nested directory
        for _nf in "$_nested_dir"/*; do
          [[ -e "$_nf" ]] || continue
          local _nfb
          _nfb="$(basename "$_nf")"
          [[ -e "$_parent_dir/$_nfb" ]] || cp -R "$_nf" "$_parent_dir/$_nfb"
        done
        rm -rf "$_nested_dir"
        echo "    cleaned up nested $_parent_name/$_nested_name/ directory"
      fi
    done < <(find "$tmp_interim" -mindepth 2 -type d -print0 2>/dev/null)

    # Set host execute permission on Amiga executables. hst-imager maps host
    # permissions to Amiga protection bits: without +x on the host file, the
    # Amiga binary gets no execute permission and cannot be run.
    for _exec_dir in C L S Devs Prefs Tools System Classes; do
      [[ -d "$tmp_interim/$_exec_dir" ]] || continue
      find "$tmp_interim/$_exec_dir" -type f ! -name "*.info" ! -name "_UAEFSDB.___" \
        -exec chmod +x {} + 2>/dev/null || true
    done
    # Also set +x on binaries inside Programs/ subdirectories (non-.info files
    # at the top level of each package, e.g. Programs/DOpus/DirectoryOpus)
    if [[ -d "$tmp_interim/Programs" ]]; then
      find "$tmp_interim/Programs" -maxdepth 2 -type f ! -name "*.info" ! -name "*.readme" \
        ! -name "*.txt" ! -name "*.doc" ! -name "*.guide" ! -name "*.readme.info" \
        ! -name "_UAEFSDB.___" ! -name "*.catalog" \
        -exec chmod +x {} + 2>/dev/null || true
    fi

    # One recursive copy is much faster than per-file fs copy calls.
    if ! "$HST_IMAGER" fs copy "$tmp_interim/" "$sys_root/" --recursive --force --makedir --quiet; then
      "$HST_IMAGER" fs copy "$tmp_interim/" "$sys_root/" --recursive --force --makedir
    fi
  fi

  # Handle User-Startup script modifications (guard against empty array)
  for script_entry in ${script_changes_to_copy[@]+"${script_changes_to_copy[@]}"}; do
    local script_target script_mode script_changes_path
    IFS='|' read -r script_target script_mode script_changes_path <<< "$script_entry"

    local script_target_norm script_changes_norm target_upper mode_lower
    script_target_norm="$(normalize_rel_path "$script_target")"
    script_changes_norm="$(normalize_rel_path "$script_changes_path")"
    target_upper="$(printf "%s" "$script_target_norm" | tr '[:lower:]' '[:upper:]')"
    mode_lower="$(printf "%s" "$script_mode" | tr '[:upper:]' '[:lower:]')"

    if [[ "$target_upper" == "S/USER-STARTUP" ]] && [[ "$mode_lower" != "false" ]]; then
      # Find the script changes file in extracted archive or Assets
      local changes_file=""
      if [[ -f "$tmp_extract/$script_changes_norm" ]]; then
        changes_file="$tmp_extract/$script_changes_norm"
      elif [[ -f "$ASSETS_DIR/AmigaFiles/$script_changes_norm" ]]; then
        changes_file="$ASSETS_DIR/AmigaFiles/$script_changes_norm"
      elif [[ -f "$ASSETS_DIR/AmigaFiles/System/$script_changes_norm" ]]; then
        changes_file="$ASSETS_DIR/AmigaFiles/System/$script_changes_norm"
      elif [[ -f "$ASSETS_DIR/../InputFiles/$script_changes_norm" ]]; then
        changes_file="$ASSETS_DIR/../InputFiles/$script_changes_norm"
      fi

      if [[ -n "$changes_file" && -f "$changes_file" ]]; then
        # Actually inject the overlay content into User-Startup (not just copy)
        local changes_basename pkg_marker
        changes_basename="$(basename "$script_changes_norm")"
        pkg_marker="${changes_basename#User-Startup_}"
        pkg_marker="${pkg_marker%.txt}"

        # First copy to S: for reference
        fs_copy_file "$changes_file" "$sys_root/S/$changes_basename" 2>/dev/null || true

        # Then inject into User-Startup
        local us_path="S/user-startup"
        if ! "$HST_IMAGER" fs dir "$sys_root/S/user-startup" >/dev/null 2>&1; then
          if "$HST_IMAGER" fs dir "$sys_root/S/User-Startup" >/dev/null 2>&1; then
            us_path="S/User-Startup"
          fi
        fi
        inject_overlay_into_script "$sys_root" "$us_path" "$changes_file" append "" "$pkg_marker" 2>/dev/null || true
      fi
    fi
  done

  # Cleanup
  rm -rf "$tmp_extract" 2>/dev/null || true
  rm -rf "$tmp_interim" 2>/dev/null || true

  if [[ "$files_staged" -gt 0 ]]; then
    echo "    installed $files_staged files at build time"
    return 0
  else
    return 1
  fi
}

normalize_roadie_install() {
  # Windows/SysInfo-style layout normalization for Roadie.
  # Keep executable/icon in canonical locations under SYS:Programs/Roadie.
  # NOTE: Nested same-name directories (Roadie/Roadie/) are now cleaned up
  # during host-side staging (install_aminet_package_buildtime). This function
  # remains as a safety net — it can copy files from nested locations but cannot
  # delete because hst-imager v1.4.x has no fs delete command.
  local sys_root="$1"
  local tmp_chk="$STAGE/roadie-normalize.$$"

  if ! "$HST_IMAGER" fs dir "$sys_root/Programs/Roadie" >/dev/null 2>&1; then
    return 0
  fi

  echo "==> Normalizing Roadie install (Windows parity)"

  # Ensure Programs/Roadie.info exists like SysInfo does.
  if ! fs_pull_file "$sys_root/Programs/Roadie.info" "$tmp_chk.info" >/dev/null 2>&1; then
    if fs_pull_file "$sys_root/Programs/Roadie/Roadie.info" "$tmp_chk.info" >/dev/null 2>&1; then
      fs_copy_file "$tmp_chk.info" "$sys_root/Programs/Roadie.info" || true
    fi
  fi

  # Guard against nested extraction layouts: Programs/Roadie/Roadie/#?
  # Use fs_pull_file checks here: hst-imager fs dir can report success for fuzzy paths.
  if ! fs_pull_file "$sys_root/Programs/Roadie/Roadie" "$tmp_chk.bin" >/dev/null 2>&1; then
    if fs_pull_file "$sys_root/Programs/Roadie/Roadie/Roadie" "$tmp_chk.bin" >/dev/null 2>&1; then
      fs_copy_file "$tmp_chk.bin" "$sys_root/Programs/Roadie/Roadie" || true
    fi
  fi
  if ! fs_pull_file "$sys_root/Programs/Roadie/Roadie.readme" "$tmp_chk.readme" >/dev/null 2>&1; then
    if fs_pull_file "$sys_root/Programs/Roadie/Roadie/Roadie.readme" "$tmp_chk.readme" >/dev/null 2>&1; then
      fs_copy_file "$tmp_chk.readme" "$sys_root/Programs/Roadie/Roadie.readme" || true
    fi
  fi
  if ! fs_pull_file "$sys_root/Programs/Roadie/Roadie.readme.info" "$tmp_chk.readme.info" >/dev/null 2>&1; then
    if fs_pull_file "$sys_root/Programs/Roadie/Roadie/Roadie.readme.info" "$tmp_chk.readme.info" >/dev/null 2>&1; then
      fs_copy_file "$tmp_chk.readme.info" "$sys_root/Programs/Roadie/Roadie.readme.info" || true
    fi
  fi

  rm -f "$tmp_chk.bin" "$tmp_chk.readme" "$tmp_chk.readme.info" "$tmp_chk.info" >/dev/null 2>&1 || true

  # The archive ships Roadie.info as a TOOL icon (for the binary).
  # When mapped to Programs/Roadie.info it must be a DRAWER icon so clicking
  # opens the drawer instead of trying to execute "Roadie" (a directory).
  if hst_amiga_available; then
    local tmp_ri="$STAGE/roadie-drawer-fix.$$"
    if fs_pull_file "$sys_root/Programs/Roadie.info" "$tmp_ri.info" >/dev/null 2>&1; then
      if "$HST_AMIGA" icon update "$tmp_ri.info" -t 2 2>/dev/null; then
        fs_copy_file "$tmp_ri.info" "$sys_root/Programs/Roadie.info"
        echo "  -> Converted Programs/Roadie.info from Tool to Drawer"
      fi
      rm -f "$tmp_ri.info" 2>/dev/null || true
    fi
  fi
}

install_ibrowse_key_file() {
  # Copy supplied IBrowse key to the installed IBrowse drawer.
  # $1 = sys_root
  # $2 = key_file (optional)
  # $3 = ibrowse_dir (optional, fallback lookup)
  local sys_root="$1"
  local key_file="${2:-}"
  local ibrowse_dir="${3:-}"

  local key_src=""
  if [[ -n "$key_file" && -f "$key_file" ]]; then
    key_src="$key_file"
  else
    local cand
    local search_paths=()
    if [[ -n "$ibrowse_dir" ]]; then
      search_paths+=(
        "$ibrowse_dir/IBrowse.key"
        "$ibrowse_dir/ibrowse.key"
        "$ibrowse_dir/IBrowse3.key"
        "$ibrowse_dir/IBrowse3Key"
      )
    fi
    search_paths+=(
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/IBrowse.key"
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/ibrowse.key"
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/IBrowse3.key"
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/IBrowse3Key"
    )
    for cand in "${search_paths[@]}"; do
      if [[ -f "$cand" ]]; then
        key_src="$cand"
        break
      fi
    done
  fi

  [[ -n "$key_src" ]] || return 0

  local prog_ib=""
  local cand_ib
  for cand_ib in "$sys_root/Programs/Ibrowse" "$sys_root/Programs/IBrowse" "$sys_root/Programs/ibrowse"; do
    if "$HST_IMAGER" fs dir "$cand_ib" >/dev/null 2>&1; then
      prog_ib="$cand_ib"
      break
    fi
  done
  if [[ -z "$prog_ib" ]]; then
    echo "  -> WARN: IBrowse key provided, but SYS:Programs/Ibrowse was not found"
    return 0
  fi

  local key_name
  key_name="$(basename "$key_src")"
  echo "  -> Installing IBrowse key file: $key_name"

  fs_copy_file "$key_src" "$prog_ib/$key_name"
  fs_copy_file "$key_src" "$prog_ib/IBrowse.key"

  if ! img_dir_has_entry_ci "$prog_ib" "IBrowse.key"; then
    die "IBrowse key was provided but not copied to SYS:Programs/Ibrowse."
  fi
}

# ensure_p96_videocore_driver()
#   Ensure VideoCore/Uaegfx board drivers exist for PiStorm/UAE RTG.
#   These drivers are not present in the standard iComp Picasso96 archive.
#   We search LocalAmigaPackages and (optionally) the Aminet Picasso96 LHA.
#   $1 = sys_root
ensure_p96_videocore_driver() {
  local sys_root="$1"
  local tmp_check="$STAGE/p96-driver-check.$$.$RANDOM"
  local need_vc=1
  local need_uae=1

  if fs_pull_file "$sys_root/Libs/Picasso96/VideoCore.card" "$tmp_check" >/dev/null 2>&1; then
    need_vc=0
  fi
  if fs_pull_file "$sys_root/Libs/Picasso96/Uaegfx.card" "$tmp_check" >/dev/null 2>&1; then
    need_uae=0
  fi
  rm -f "$tmp_check" 2>/dev/null || true

  [[ "$need_vc" -eq 1 || "$need_uae" -eq 1 ]] || return 0

  echo "==> P96: checking for missing VideoCore/Uaegfx drivers"

  # 1) LocalAmigaPackages overrides (skip in download-only mode)
  if [[ "$DOWNLOAD_ONLY" != "1" ]]; then
    local driver_dirs=(
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/P96Drivers"
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages"
    )
    for src_dir in "${driver_dirs[@]}"; do
      [[ -d "$src_dir" ]] || continue
      if [[ "$need_vc" -eq 1 && -f "$src_dir/VideoCore.card" ]]; then
        echo "==> P96: using VideoCore.card from $src_dir"
        local tmp_vc="$STAGE/VideoCore.card.$$.$RANDOM"
        cp -f "$src_dir/VideoCore.card" "$tmp_vc"
        write_uaem_with_perms "----rwed" "$tmp_vc"
        ensure_img_dir "$sys_root/Libs/Picasso96"
        fs_copy_file "$tmp_vc" "$sys_root/Libs/Picasso96/VideoCore.card" --uaemetadata UaeMetafile
        need_vc=0
      fi
      if [[ "$need_uae" -eq 1 && -f "$src_dir/Uaegfx.card" ]]; then
        echo "==> P96: using Uaegfx.card from $src_dir"
        local tmp_uae="$STAGE/Uaegfx.card.$$.$RANDOM"
        cp -f "$src_dir/Uaegfx.card" "$tmp_uae"
        write_uaem_with_perms "----rwed" "$tmp_uae"
        ensure_img_dir "$sys_root/Libs/Picasso96"
        fs_copy_file "$tmp_uae" "$sys_root/Libs/Picasso96/Uaegfx.card" --uaemetadata UaeMetafile
        need_uae=0
      fi
    done
  fi

  # 2) Aminet Picasso96 shareware (if available)
  if [[ "$need_vc" -eq 1 || "$need_uae" -eq 1 ]]; then
    if [[ -n "${AMINET_DIR:-}" && -d "${AMINET_DIR:-}" ]]; then
      local aminet_p96=""
      for cand in "$AMINET_DIR"/Picasso96.lha "$AMINET_DIR"/picasso96.lha; do
        [[ -f "$cand" ]] && aminet_p96="$cand" && break
      done
      if [[ -n "$aminet_p96" ]]; then
        echo "==> P96: extracting drivers from Aminet $(basename "$aminet_p96")"
        local tmp_p96="$STAGE/p96-aminet-extract.$$.$RANDOM"
        rm -rf "$tmp_p96" 2>/dev/null || true
        mkdir -p "$tmp_p96"
        "$HST_IMAGER" fs extract "$aminet_p96" "$tmp_p96" --recursive --force 2>/dev/null || \
        "$HST_IMAGER" fs extract "$aminet_p96" "$tmp_p96" --recursive 2>/dev/null || {
          echo "==> WARN: Failed to extract Aminet Picasso96 LHA for driver scan"
        }

        if [[ -d "$tmp_p96" ]]; then
          if [[ "$need_vc" -eq 1 ]]; then
            local vc_card
            vc_card="$(find "$tmp_p96" -iname "VideoCore.card" 2>/dev/null | head -n1)"
            if [[ -n "$vc_card" ]]; then
              echo "==> P96: found VideoCore.card in Aminet shareware"
              local tmp_vc="$STAGE/VideoCore.card.$$.$RANDOM"
              cp -f "$vc_card" "$tmp_vc"
              write_uaem_with_perms "----rwed" "$tmp_vc"
              ensure_img_dir "$sys_root/Libs/Picasso96"
              fs_copy_file "$tmp_vc" "$sys_root/Libs/Picasso96/VideoCore.card" --uaemetadata UaeMetafile
              need_vc=0
            fi
          fi
          if [[ "$need_uae" -eq 1 ]]; then
            local uae_card
            uae_card="$(find "$tmp_p96" -iname "Uaegfx.card" 2>/dev/null | head -n1)"
            if [[ -n "$uae_card" ]]; then
              echo "==> P96: found Uaegfx.card in Aminet shareware"
              local tmp_uae="$STAGE/Uaegfx.card.$$.$RANDOM"
              cp -f "$uae_card" "$tmp_uae"
              write_uaem_with_perms "----rwed" "$tmp_uae"
              ensure_img_dir "$sys_root/Libs/Picasso96"
              fs_copy_file "$tmp_uae" "$sys_root/Libs/Picasso96/Uaegfx.card" --uaemetadata UaeMetafile
              need_uae=0
            fi
          fi
        fi

        rm -rf "$tmp_p96" 2>/dev/null || true
      fi
    fi
  fi

  if [[ "$need_vc" -eq 1 || "$need_uae" -eq 1 ]]; then
    echo "==> WARN: P96 drivers missing: VideoCore.card=${need_vc} Uaegfx.card=${need_uae}"
    echo "==> Place VideoCore.card/Uaegfx.card in Assets/LocalAmigaPackages/P96Drivers"
  fi
}

# install_ibrowse_from_official()
#   Install IBrowse and AmiSSL from official packages (ibrowse-dev.net).
#   $1 = sys_root
#   $2 = ibrowse_dir (directory containing downloaded .lha files)
#   $3 = key_file (optional IBrowse registration key)
install_ibrowse_from_official() {
  local sys_root="$1"
  local ibrowse_dir="$2"
  local key_file="${3:-}"

  echo "==> Installing IBrowse/AmiSSL from official packages"

  # Install IBrowse if present
  local ibrowse_lha=""
  for f in "$ibrowse_dir"/IBrowse*.lha; do
    [[ -f "$f" ]] && ibrowse_lha="$f" && break
  done

  local ibrowse_installed=0
  if [[ -n "$ibrowse_lha" ]]; then
    echo "  -> Installing IBrowse from: $(basename "$ibrowse_lha")"
    local tmp_ib="$STAGE/ibrowse-extract"
    rm -rf "$tmp_ib" 2>/dev/null || true
    mkdir -p "$tmp_ib"

    "$HST_IMAGER" fs extract "$ibrowse_lha" "$tmp_ib" --recursive --force 2>/dev/null || \
    "$HST_IMAGER" fs extract "$ibrowse_lha" "$tmp_ib" --recursive 2>/dev/null || {
      echo "  -> WARN: Failed to extract IBrowse"
    }

    # Debug: show extracted structure
    echo "  -> DEBUG: IBrowse archive contents:"
    find "$tmp_ib" -maxdepth 3 -type d 2>/dev/null | head -20 | while read d; do echo "     DIR: $d"; done
    find "$tmp_ib" -maxdepth 3 -type f -name "*.codec" 2>/dev/null | head -10 | while read f; do echo "     CODEC: $f"; done
    find "$tmp_ib" -maxdepth 3 -type f -name "spatch" 2>/dev/null | while read f; do echo "     SPATCH: $f"; done

    # Find IBrowse root (usually IBrowse3.0-OS3/)
    local ib_root=""
    ib_root="$(find "$tmp_ib" -maxdepth 1 -type d -name "IBrowse*" | head -n1)"
    if [[ -n "$ib_root" && -d "$ib_root" ]]; then
      echo "  -> Found IBrowse root: $(basename "$ib_root")"

      # Copy TransferAnimation from Anims/ to Images/ on host BEFORE copying to image.
      # IBrowse defaults look for TransferAnimation in PROGDIR:Images/ but the
      # archive ships it in a separate Anims/ directory.
      local ib_images_dir=""
      ib_images_dir="$(find "$ib_root" -maxdepth 1 -type d -iname "images" 2>/dev/null | head -n1)"
      local ib_anims_dir=""
      ib_anims_dir="$(find "$ib_root" -maxdepth 1 -type d -iname "anims" 2>/dev/null | head -n1)"
      if [[ -n "$ib_anims_dir" && -d "$ib_anims_dir" && -n "$ib_images_dir" && -d "$ib_images_dir" ]]; then
        local anim_file
        for anim_file in "$ib_anims_dir"/*; do
          [[ -f "$anim_file" ]] || continue
          local anim_name
          anim_name="$(basename "$anim_file")"
          if [[ ! -f "$ib_images_dir/$anim_name" ]]; then
            cp "$anim_file" "$ib_images_dir/$anim_name" 2>/dev/null || true
            echo "  -> Copied $anim_name from Anims/ to Images/ for default path"
          fi
        done
      fi

      # Install to Programs/Ibrowse (lowercase 'b' for compatibility with existing scripts)
      ensure_img_dir "$sys_root/Programs/Ibrowse"
      fs_copy_tree_filtered "$ib_root" "$sys_root/Programs/Ibrowse"

      # Copy IBrowse.info from Icons/ subdirectory to make the binary visible on Workbench
      # The official archive stores icons separately in Icons/ directory
      if [[ -f "$ib_root/Icons/IBrowse.info" ]]; then
        echo "  -> Copying IBrowse.info from Icons/ to make binary visible"
        fs_copy_file "$ib_root/Icons/IBrowse.info" "$sys_root/Programs/Ibrowse/IBrowse.info"
      fi

      # Copy drawer icon for Programs/Ibrowse
      local found_drawer_info=0
      # First try IBrowseDrw.info from Icons/ directory
      if [[ -f "$ib_root/Icons/IBrowseDrw.info" ]]; then
        fs_copy_file "$ib_root/Icons/IBrowseDrw.info" "$sys_root/Programs/Ibrowse.info"
        echo "  -> Copied drawer icon from Icons/IBrowseDrw.info"
        found_drawer_info=1
      fi

      # Fallback: check for drawer icon at archive root level
      if [[ "$found_drawer_info" -eq 0 ]]; then
        for info_name in "IBrowse3.0-OS3.info" "IBrowse.info" "ibrowse.info"; do
          if [[ -f "$tmp_ib/$info_name" ]]; then
            fs_copy_file "$tmp_ib/$info_name" "$sys_root/Programs/Ibrowse.info"
            echo "  -> Copied drawer icon: $info_name"
            found_drawer_info=1
            break
          fi
        done
      fi

      # Install spatch to C: - check multiple locations (case-insensitive)
      local spatch_path=""
      spatch_path="$(find "$ib_root" -maxdepth 2 -type f -iname "spatch" 2>/dev/null | head -n1)"
      if [[ -n "$spatch_path" && -f "$spatch_path" ]]; then
        echo "  -> Installing spatch to C: (from $(basename "$(dirname "$spatch_path")")/)"
        fs_copy_file "$spatch_path" "$sys_root/C/spatch"
      else
        echo "  -> WARN: spatch not found in IBrowse archive"
      fi

      # Verify codecs were copied (case-insensitive search)
      local codec_dir=""
      codec_dir="$(find "$ib_root" -maxdepth 1 -type d -iname "codecs" 2>/dev/null | head -n1)"
      if [[ -n "$codec_dir" && -d "$codec_dir" ]]; then
        local codec_count
        codec_count="$(find "$codec_dir" -maxdepth 1 -type f -name "*.codec" 2>/dev/null | wc -l | tr -d ' ')"
        echo "  -> Found $codec_count codecs in: $(basename "$codec_dir")"

        # Check for patch directories (case-insensitive)
        local patch_dir=""
        patch_dir="$(find "$ib_root" -maxdepth 1 -type d -iname "codecs040fpu" 2>/dev/null | head -n1)"
        if [[ -n "$patch_dir" && -d "$patch_dir" ]]; then
          local patch_count
          patch_count="$(find "$patch_dir" -maxdepth 1 -type f -name "*.spatch" 2>/dev/null | wc -l | tr -d ' ')"
          echo "  -> Found $patch_count spatch files in: $(basename "$patch_dir")"
        else
          echo "  -> WARN: No codecs040fpu directory found for codec patching"
        fi
      else
        echo "  -> WARN: No codecs directory found in IBrowse archive"
      fi

      # Install IBrowse MUI custom classes to MUI:Libs/mui/ (Programs/MUI/Libs/mui/)
      # IBrowse ships NList.mcc, NListtree.mcc, NBitmap.mcc, etc. in its own mui/ dir
      # but MUI only searches LIBS:MUI/ for custom classes (= MUI:Libs/mui/ via assigns).
      local ib_mui_dir=""
      ib_mui_dir="$(find "$ib_root" -maxdepth 1 -type d -iname "mui" 2>/dev/null | head -n1)"
      if [[ -n "$ib_mui_dir" && -d "$ib_mui_dir" ]]; then
        ensure_img_dir "$sys_root/Programs/MUI/Libs/mui"
        local mcc_count=0
        for mcc_file in "$ib_mui_dir"/*.mcc "$ib_mui_dir"/*.mcp; do
          [[ -f "$mcc_file" ]] || continue
          local mcc_name
          mcc_name="$(basename "$mcc_file")"
          fs_copy_file "$mcc_file" "$sys_root/Programs/MUI/Libs/mui/$mcc_name"
          mcc_count=$((mcc_count + 1))
        done
        echo "  -> Installed $mcc_count IBrowse MUI classes to MUI:Libs/mui/"
      fi

      # Install IBrowse libraries (javascript.library, etc.) to SYS:Libs/
      # IBrowse ships these in its own Libs/ subdirectory, but AmigaOS
      # OpenLibrary() only searches LIBS: (= SYS:Libs/).  Without this,
      # IBrowse shows "Unable to open javascript.library" at launch.
      local ib_libs_dir=""
      ib_libs_dir="$(find "$ib_root" -maxdepth 1 -type d -iname "libs" 2>/dev/null | head -n1)"
      if [[ -z "$ib_libs_dir" ]]; then
        # Some archive layouts put Libs/ outside the main IBrowse directory
        ib_libs_dir="$(find "$tmp_ib" -maxdepth 1 -type d -iname "libs" 2>/dev/null | head -n1)"
      fi
      if [[ -n "$ib_libs_dir" && -d "$ib_libs_dir" ]]; then
        ensure_img_dir "$sys_root/Libs"
        local lib_count=0
        for lib_file in "$ib_libs_dir"/*.library; do
          [[ -f "$lib_file" ]] || continue
          local lib_name
          lib_name="$(basename "$lib_file")"
          fs_copy_file "$lib_file" "$sys_root/Libs/$lib_name"
          echo "  -> Installed $lib_name to SYS:Libs/"
          lib_count=$((lib_count + 1))
        done
        if [[ $lib_count -eq 0 ]]; then
          echo "  -> No .library files found in IBrowse Libs/ directory"
        fi
      else
        # Last resort: search entire extraction for javascript.library
        local js_lib=""
        js_lib="$(find "$tmp_ib" -type f -iname "javascript.library" 2>/dev/null | head -n1)"
        if [[ -n "$js_lib" && -f "$js_lib" ]]; then
          ensure_img_dir "$sys_root/Libs"
          fs_copy_file "$js_lib" "$sys_root/Libs/javascript.library"
          echo "  -> Installed javascript.library to SYS:Libs/ (from $(dirname "$js_lib"))"
        else
          echo "  -> WARN: javascript.library not found in IBrowse archive"
        fi
      fi

      # Verify Images/ directory was copied (TransferAnimation, StatusBar, etc.)
      # IBrowse uses PROGDIR:Images/ for toolbar and status bar graphics.
      # (Anims/ → Images/ copy already done above, before fs_copy_tree_filtered)
      if [[ -n "${ib_images_dir:-}" && -d "${ib_images_dir:-}" ]]; then
        local img_count
        img_count="$(find "$ib_images_dir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')"
        echo "  -> Found $img_count image files in: $(basename "$ib_images_dir")/"
      else
        echo "  -> WARN: No Images/ directory found in IBrowse archive"
      fi

      # Use archive's own prefs if present; otherwise let IBrowse create defaults
      # on first launch (built-in defaults include correct image/anim paths).
      # The bundled prefs lacks PROGDIR:images/ and PROGDIR:Anims/ settings which
      # causes "unable to open image file" errors for Picture/Navigation/StatusBar/
      # TransferAnimation on startup.
      if [[ -f "$ib_root/IBrowse.prefs" ]]; then
        echo "  -> Archive includes IBrowse.prefs — keeping archive version"
      else
        echo "  -> No IBrowse.prefs in archive — IBrowse will create defaults on first launch"
      fi

      # Ensure cache directory exists at build time
      ensure_img_dir "$sys_root/Programs/Ibrowse/cache"

      ibrowse_installed=1
      echo "  -> IBrowse installed to Programs/Ibrowse"
    else
      echo "  -> WARN: Could not find IBrowse root directory in archive"
      echo "  -> DEBUG: Top-level contents:"
      ls -la "$tmp_ib" 2>/dev/null | head -10
    fi

    rm -rf "$tmp_ib" 2>/dev/null || true
  fi

  # Install AmiSSL if present
  local amissl_lha=""
  for f in "$ibrowse_dir"/AmiSSL*.lha; do
    [[ -f "$f" ]] && amissl_lha="$f" && break
  done

  if [[ -n "$amissl_lha" ]]; then
    echo "  -> Installing AmiSSL from: $(basename "$amissl_lha")"
    local tmp_ssl="$STAGE/amissl-extract"
    rm -rf "$tmp_ssl" 2>/dev/null || true
    mkdir -p "$tmp_ssl"

    "$HST_IMAGER" fs extract "$amissl_lha" "$tmp_ssl" --recursive --force 2>/dev/null || \
    "$HST_IMAGER" fs extract "$amissl_lha" "$tmp_ssl" --recursive 2>/dev/null || {
      echo "  -> WARN: Failed to extract AmiSSL"
    }

    # Install AmiSSL components to Programs/AmiSSL/ (matching Windows reference layout).
    # The AmiSSL: assign (User-Startup) points here; AmiSSL uses AmiSSL:Certs/ for CA
    # certificates and AmiSSL:Libs/ is added to LIBS: for library resolution.
    local ssl_root=""
    ssl_root="$(find "$tmp_ssl" -maxdepth 1 -type d -name "AmiSSL*" | head -n1)"
    if [[ -n "$ssl_root" && -d "$ssl_root" ]]; then
      local amissl_dest="$sys_root/Programs/AmiSSL"

      # Install amisslmaster.library to both Programs/AmiSSL/Libs/ and SYS:Libs/
      ensure_img_dir "$amissl_dest/Libs"
      ensure_img_dir "$sys_root/Libs"
      if [[ -f "$ssl_root/Libs/AmigaOS3/amisslmaster.library" ]]; then
        fs_copy_file "$ssl_root/Libs/AmigaOS3/amisslmaster.library" "$amissl_dest/Libs/amisslmaster.library"
        fs_copy_file "$ssl_root/Libs/AmigaOS3/amisslmaster.library" "$sys_root/Libs/amisslmaster.library"
      fi

      # Install versioned AmiSSL libraries to Programs/AmiSSL/Libs/AmiSSL/
      ensure_img_dir "$amissl_dest/Libs/AmiSSL"
      if [[ -d "$ssl_root/Libs/AmigaOS3/AmiSSL" ]]; then
        # Prefer 68020-40 version for Emu68
        if [[ -d "$ssl_root/Libs/AmigaOS3/AmiSSL/68020-40" ]]; then
          for lib in "$ssl_root/Libs/AmigaOS3/AmiSSL/68020-40"/*.library; do
            [[ -f "$lib" ]] || continue
            fs_copy_file "$lib" "$amissl_dest/Libs/AmiSSL/$(basename "$lib")"
          done
        fi
      fi

      # Install CA certificates to Programs/AmiSSL/Certs/
      if [[ -d "$ssl_root/Certs" ]]; then
        fs_copy_tree_filtered "$ssl_root/Certs" "$amissl_dest/Certs"
      fi

      # Install OpenSSL command to C:
      ensure_img_dir "$sys_root/C"
      if [[ -f "$ssl_root/C/AmigaOS3/OpenSSL" ]]; then
        fs_copy_file "$ssl_root/C/AmigaOS3/OpenSSL" "$sys_root/C/OpenSSL"
      fi

      echo "  -> AmiSSL installed to Programs/AmiSSL/"
    fi

    rm -rf "$tmp_ssl" 2>/dev/null || true
  fi

  install_ibrowse_key_file "$sys_root" "$key_file" "$ibrowse_dir"

  # Create OneTimeRun/Ibrowse script for codec patching (040fpu for Emu68)
  if [[ "$ibrowse_installed" -eq 1 ]]; then
    local tmp_ib_script="$STAGE/Ibrowse.firstboot"
    cat > "$tmp_ib_script" <<'__IBROWSE_PATCH__'
; Amiga Imager: IBrowse codec patching for 040/FPU (Emu68)
; Installed from official ibrowse-dev.net
FAILAT 21

; Ensure Programs drawer has an icon (visible in Workbench)
IF NOT EXISTS SYS:Programs.info
  IF EXISTS SYS:Prefs/Env-Archive/Sys/def_drawer.info
    COPY SYS:Prefs/Env-Archive/Sys/def_drawer.info SYS:Programs.info CLONE QUIET
  ELSE
    IF EXISTS ENV:Sys/def_drawer.info
      COPY ENV:Sys/def_drawer.info SYS:Programs.info CLONE QUIET
    ENDIF
  ENDIF
ENDIF

; Check prerequisites for codec patching
IF NOT EXISTS C:spatch
  ECHO "spatch not found, skipping codec patching"
  SKIP END
ENDIF

IF NOT EXISTS SYS:Programs/Ibrowse/codecs/gif.codec
  ECHO "IBrowse codecs not found, skipping"
  SKIP END
ENDIF

IF NOT EXISTS SYS:Programs/Ibrowse/codecs040fpu/gif.codec.spatch
  ECHO "040fpu patches not found, skipping"
  SKIP END
ENDIF

ECHO "Patching IBrowse codecs for 040/FPU..."

C:spatch >NIL: -oSYS:Programs/Ibrowse/codecs/gif.codec -pSYS:Programs/Ibrowse/codecs040fpu/gif.codec.spatch SYS:Programs/Ibrowse/codecs/gif.codec
C:spatch >NIL: -oSYS:Programs/Ibrowse/codecs/jpeg.codec -pSYS:Programs/Ibrowse/codecs040fpu/jpeg.codec.spatch SYS:Programs/Ibrowse/codecs/jpeg.codec
C:spatch >NIL: -oSYS:Programs/Ibrowse/codecs/png.codec -pSYS:Programs/Ibrowse/codecs040fpu/png.codec.spatch SYS:Programs/Ibrowse/codecs/png.codec
C:spatch >NIL: -oSYS:Programs/Ibrowse/codecs/xbm.codec -pSYS:Programs/Ibrowse/codecs040fpu/xbm.codec.spatch SYS:Programs/Ibrowse/codecs/xbm.codec

IF EXISTS SYS:Programs/Ibrowse/codecs/imagedecode.module
  IF EXISTS SYS:Programs/Ibrowse/codecs040fpu/imagedecode.module.spatch
    C:spatch >NIL: -oSYS:Programs/Ibrowse/codecs/imagedecode.module -pSYS:Programs/Ibrowse/codecs040fpu/imagedecode.module.spatch SYS:Programs/Ibrowse/codecs/imagedecode.module
  ENDIF
ENDIF

; Clean up patch files
DELETE SYS:Programs/Ibrowse/codecs020fpu ALL FORCE QUIET
DELETE SYS:Programs/Ibrowse/codecs040fpu ALL FORCE QUIET
DELETE SYS:Programs/Ibrowse/codecs060fpu ALL FORCE QUIET

; Ensure cache directory exists
MAKEDIR >NIL: SYS:Programs/Ibrowse/cache

ECHO "IBrowse codec patching complete."

LAB END
__IBROWSE_PATCH__
    make_lf_only "$tmp_ib_script"
    ensure_img_dir "$sys_root/S/OneTimeRun"
    fs_copy_file "$tmp_ib_script" "$sys_root/S/OneTimeRun/Ibrowse"
    echo "  -> Created OneTimeRun/Ibrowse script for codec patching"

    # Verify installation on image
    echo "  -> DEBUG: Verifying IBrowse installation on image:"
    echo "     Programs/Ibrowse contents:"
    "$HST_IMAGER" fs dir "$sys_root/Programs/Ibrowse" 2>/dev/null | head -20 || echo "     (failed to list)"
    echo "     Programs/Ibrowse/Images contents:"
    "$HST_IMAGER" fs dir "$sys_root/Programs/Ibrowse/Images" 2>/dev/null | head -20 || echo "     (failed to list or not present)"
    echo "     Programs/Ibrowse/codecs contents:"
    "$HST_IMAGER" fs dir "$sys_root/Programs/Ibrowse/codecs" 2>/dev/null | head -10 || echo "     (failed to list or not present)"
    echo "     Programs/Ibrowse/codecs040fpu contents:"
    "$HST_IMAGER" fs dir "$sys_root/Programs/Ibrowse/codecs040fpu" 2>/dev/null | head -10 || echo "     (failed to list or not present)"
    echo "     SYS:Libs/ IBrowse libraries:"
    "$HST_IMAGER" fs dir "$sys_root/Libs/javascript.library" 2>/dev/null || echo "     javascript.library: NOT present"
    echo "     C/spatch check:"
    "$HST_IMAGER" fs dir "$sys_root/C/spatch" 2>/dev/null || echo "     (not present)"
  fi
}

# install_wbdock()
#   Extract and install WBDock2 dock launcher into Programs/WBDock.
#   Creates a delayed launcher script (S/StartWBDock) that waits for IPrefs
#   to switch to RTG before starting WBDock.  The launcher is invoked from
#   the Startup-Sequence by relocate_iprefs_after_loadwb().
#   $1 = sys_root   $2 = ibrowse_dir (staging dir containing wbdock2.lha)
install_wbdock() {
  local sys_root="$1"
  local ibrowse_dir="$2"

  echo "==> Installing WBDock2"

  local wbdock_lha=""
  for f in "$ibrowse_dir"/wbdock2.lha "$ibrowse_dir"/WBDock2.lha; do
    [[ -f "$f" ]] && wbdock_lha="$f" && break
  done

  if [[ -z "$wbdock_lha" ]]; then
    echo "==> WARN: WBDock2 LHA not found in staging dir"
    return 0
  fi

  local tmp_wd="$STAGE/wbdock-extract"
  rm -rf "$tmp_wd" 2>/dev/null || true
  mkdir -p "$tmp_wd"

  # Try lha first (check common locations since PATH may be minimal)
  local _wd_extracted=0
  local _lha_bin=""
  for _l in lha /usr/local/bin/lha /opt/homebrew/bin/lha; do
    command -v "$_l" >/dev/null 2>&1 && _lha_bin="$_l" && break
    [[ -x "$_l" ]] && _lha_bin="$_l" && break
  done
  if [[ -n "$_lha_bin" ]]; then
    (cd "$tmp_wd" && "$_lha_bin" x "$wbdock_lha") && _wd_extracted=1
  fi
  if (( _wd_extracted == 0 )); then
    "$HST_IMAGER" fs extract "$wbdock_lha" "$tmp_wd" --recursive --force 2>/dev/null || \
    "$HST_IMAGER" fs extract "$wbdock_lha" "$tmp_wd" --recursive 2>/dev/null || {
      echo "==> WARN: Failed to extract WBDock2"
      return 0
    }
  fi

  # Find WBDock2 root directory
  local wd_root=""
  wd_root="$(find "$tmp_wd" -maxdepth 1 -type d -name "WBDock2" | head -n1)"
  if [[ -z "$wd_root" || ! -d "$wd_root" ]]; then
    echo "==> WARN: WBDock2 directory not found in archive"
    return 0
  fi

  # Install to Programs/WBDock/ (NOT WBStartup — avoids race with IPrefs RTG switch).
  # A delayed launcher script (S/StartWBDock) is created below and invoked from
  # the Startup-Sequence after IPrefs, giving IPrefs time to switch to RTG first.
  local wd_dst="$sys_root/Programs/WBDock"
  ensure_img_dir "$wd_dst"
  fs_copy_tree_filtered "$wd_root" "$wd_dst"

  # Copy apps directory (dock plugins) — WBDock looks in PROGDIR:apps/
  if [[ -d "$wd_root/apps" ]]; then
    ensure_img_dir "$wd_dst/apps"
    fs_copy_tree_filtered "$wd_root/apps" "$wd_dst/apps"
  fi

  # Copy Skins directory — WBDock looks in PROGDIR:Skins/
  if [[ -d "$wd_root/Skins" ]]; then
    ensure_img_dir "$wd_dst/Skins"
    fs_copy_tree_filtered "$wd_root/Skins" "$wd_dst/Skins"
  fi

  # Write custom WBDock.config (one Amiga path per line, matching archive format).
  # Must use exact filename "WBDock.config" to match what the archive ships.
  local cfg_host="$STAGE/WBDock.config"
  cat > "$cfg_host" <<'WBDOCKCFG'
SYS:System/Shell
SYS:Tools/HDToolBox
SYS:Prefs/ScreenMode
SYS:Programs/Ibrowse/IBrowse
SYS:Programs/DOpus/DirectoryOpus
SYS:Programs/IMP3
SYS:PiStorm/OnlineWifi
WBDOCKCFG
  make_lf_only "$cfg_host"
  fs_copy_file "$cfg_host" "$wd_dst/WBDock.config"

  # Create delayed launcher script: waits 3s for IPrefs to apply RTG, then starts WBDock.
  # Invoked from Startup-Sequence via: Run >NIL: Execute SYS:S/StartWBDock
  local launcher="$STAGE/StartWBDock"
  cat > "$launcher" <<'__STARTWBDOCK__'
; Amiga Imager: delayed WBDock launcher
; Wait for IPrefs to switch to RTG screen mode before opening dock
Wait 3
; CD to WBDock directory so relative paths (config, patterns, textures) resolve
CD SYS:Programs/WBDock
Run <>NIL: WBDock
__STARTWBDOCK__
  make_lf_only "$launcher"
  ensure_img_dir "$sys_root/S"
  fs_copy_file "$launcher" "$sys_root/S/StartWBDock"

  # Set Tool Manager display style via tooltype on WBDock.info.
  # WBDock reads PROGDIR:<name>.info for settings when launched from CLI too.
  local wd_info="$STAGE/WBDock.info"
  if fs_pull_file "$wd_dst/WBDock.info" "$wd_info" >/dev/null 2>&1; then
    modify_tooltype "$wd_info" "STYLE" "TOOLMANAGER"
    fs_copy_file "$wd_info" "$wd_dst/WBDock.info"
    rm -f "$wd_info" 2>/dev/null || true
  else
    echo "==> WARN: WBDock.info not found — cannot set STYLE=TOOLMANAGER"
  fi

  echo "==> WBDock2 installed to Programs/WBDock (STYLE=TOOLMANAGER, delayed launch via S/StartWBDock)"
}

# configure_newmeter_position()
#   Delay NewMeter startup so it opens after IPrefs switches to RTG.
#   Without delay, NewMeter sees the native screen dimensions and XPOS=-8
#   (right edge) lands in the center of the larger RTG screen.
#   $1 = sys_root (hst-imager virtual path to the System partition)
configure_newmeter_position() {
  local sys_root="$1"

  echo "==> Configuring NewMeter startup delay for RTG"

  local tmp_info="$STAGE/NewMeter.info"

  # Pull NewMeter.info from WBStartup/Disabled (installed there by CSV)
  if ! fs_pull_file "$sys_root/WBStartup/Disabled/NewMeter.info" "$tmp_info" >/dev/null 2>&1; then
    # Try WBStartup directly as fallback
    if ! fs_pull_file "$sys_root/WBStartup/NewMeter.info" "$tmp_info" >/dev/null 2>&1; then
      echo "==> WARN: NewMeter.info not found — skipping position configuration"
      return 0
    fi
  fi

  # NewMeter starts from WBStartup which runs when LoadWB opens the native
  # screen (before IPrefs switches to RTG). With DELAY=1, NewMeter opens its
  # window immediately on the ~640px native screen, so XPOS=-8 lands at ~632px
  # — the center of a 1280px RTG screen. Increase DELAY so NewMeter waits for
  # IPrefs to apply RTG before opening; then XPOS=-8 correctly resolves to
  # 8 pixels from the right edge of the 1280px RTG screen.
  modify_tooltype "$tmp_info" "DELAY" "5"

  # Copy modified .info back to image
  fs_copy_file "$tmp_info" "$sys_root/WBStartup/Disabled/NewMeter.info"

  echo "==> NewMeter startup delayed (DELAY=5) for IPrefs RTG switch"
  rm -f "$tmp_info" 2>/dev/null || true
}


# install_emu68_tools()
#   Download latest Emu68-tools release and overlay its contents into SYS:.
#   This keeps VideoCore.card and related tools up to date (Windows parity).
#   $1 = sys_root
install_emu68_tools() {
  local sys_root="$1"

  echo "==> Emu68-tools: downloading latest release"

  if ! have_cmd unzip; then
    echo "==> WARN: unzip not found; cannot install Emu68-tools"
    return 0
  fi

  local tools_url=""
  tools_url="$(resolve_emu68_tools_zip_url || true)"
  if [[ -z "$tools_url" ]]; then
    echo "==> WARN: Emu68-tools URL not resolved; skipping download"
    return 0
  fi

  local tools_zip="$STAGE/Emu68-tools.zip"
  local tools_extract="$STAGE/emu68-tools-extract"

  rm -rf "$tools_extract" 2>/dev/null || true
  mkdir -p "$tools_extract"

  if ! curl -fL --retry 3 --retry-delay 2 -o "$tools_zip" "$tools_url"; then
    echo "==> WARN: Failed to download Emu68-tools from $tools_url"
    return 0
  fi

  if ! unzip -q "$tools_zip" -d "$tools_extract"; then
    echo "==> WARN: Failed to unzip Emu68-tools"
    return 0
  fi

  # Detect actual root: if there is a single top-level directory, use it.
  local root="$tools_extract"
  local topdir_count topfile_count
  topdir_count="$(find "$tools_extract" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
  topfile_count="$(find "$tools_extract" -mindepth 1 -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')"
  if [[ "$topdir_count" -eq 1 && "$topfile_count" -eq 0 ]]; then
    root="$(find "$tools_extract" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
  fi
  echo "==> Emu68-tools root: $(basename "$root")"

  local dir
  for dir in C Devs Libs Prefs PiStorm Tools Utilities Storage Resources L Locale Classes S Rexxc; do
    if [[ -d "$root/$dir" ]]; then
      echo "==> Emu68-tools overlay: $dir"
      fs_copy_tree_filtered "$root/$dir" "$sys_root/$dir"
      apply_script_bits_for_tree "$root/$dir" "$sys_root/$dir"
    fi
  done

  # Ensure VideoCore/Uaegfx card drivers are present (some releases store them outside Libs/Picasso96).
  local vc_card uae_card tmp_card
  vc_card="$(find "$root" -type f -iname 'VideoCore.card' 2>/dev/null | head -n 1 || true)"
  uae_card="$(find "$root" -type f -iname 'Uaegfx.card' 2>/dev/null | head -n 1 || true)"

  if [[ -n "$vc_card" ]]; then
    echo "==> Emu68-tools: installing VideoCore.card"
    tmp_card="$STAGE/VideoCore.card.$$.$RANDOM"
    cp -f "$vc_card" "$tmp_card"
    write_uaem_with_perms "----rwed" "$tmp_card"
    ensure_img_dir "$sys_root/Libs/Picasso96"
    fs_copy_file "$tmp_card" "$sys_root/Libs/Picasso96/VideoCore.card" --uaemetadata UaeMetafile
  fi
  if [[ -n "$uae_card" ]]; then
    echo "==> Emu68-tools: installing Uaegfx.card"
    tmp_card="$STAGE/Uaegfx.card.$$.$RANDOM"
    cp -f "$uae_card" "$tmp_card"
    write_uaem_with_perms "----rwed" "$tmp_card"
    ensure_img_dir "$sys_root/Libs/Picasso96"
    fs_copy_file "$tmp_card" "$sys_root/Libs/Picasso96/Uaegfx.card" --uaemetadata UaeMetafile
  fi

  # Prefer Emu68Info binary from tools over the stub (avoids script-bit issues).
  local emu68info_bin=""
  if [[ -d "$root/C" ]]; then
    emu68info_bin="$(find "$root/C" -type f \( -iname 'Emu68Info' -o -iname 'EMU68INFO' \) 2>/dev/null | head -n 1 || true)"
  fi
  if [[ -z "$emu68info_bin" ]]; then
    emu68info_bin="$(find "$root" -type f \( -iname 'Emu68Info' -o -iname 'EMU68INFO' \) \
      ! -name '*.r' 2>/dev/null | head -n 1 || true)"
  fi
  if [[ -n "$emu68info_bin" ]]; then
    echo "==> Emu68-tools: installing Emu68Info -> C/EMU68INFO"
    local tmp_emu68info="$STAGE/Emu68Info.$$.$RANDOM"
    cp -f "$emu68info_bin" "$tmp_emu68info"
    write_uaem_with_perms "----rwed" "$tmp_emu68info"
    ensure_img_dir "$sys_root/C"
    fs_copy_file "$tmp_emu68info" "$sys_root/C/EMU68INFO" --uaemetadata UaeMetafile
    fs_copy_file "$tmp_emu68info" "$sys_root/C/emu68info" --uaemetadata UaeMetafile
  else
    echo "==> WARN: Emu68-tools did not contain Emu68Info binary; startup detection may fail"
  fi

  # Ensure wifipi.device is installed to Devs/Networks (Emu68-tools includes it since v1.1)
  local wifipi_dev=""
  wifipi_dev="$(find "$root" -type f -iname 'wifipi.device' 2>/dev/null | head -n 1 || true)"
  if [[ -n "$wifipi_dev" ]]; then
    echo "==> Emu68-tools: installing wifipi.device -> Devs/Networks/"
    local tmp_wifipi="$STAGE/wifipi.device.$$.$RANDOM"
    cp -f "$wifipi_dev" "$tmp_wifipi"
    write_uaem_with_perms "----rwed" "$tmp_wifipi"
    ensure_img_dir "$sys_root/Devs/Networks"
    fs_copy_file "$tmp_wifipi" "$sys_root/Devs/Networks/wifipi.device" --uaemetadata UaeMetafile
  else
    echo "==> WARN: wifipi.device not found in Emu68-tools archive"
  fi

  # Install WaitUntilConnected to C: (needed by PiStorm Online script for WiFi connections).
  # Emu68-tools stores it under WaitUntilConnected/WaitUntilConnected rather than C/.
  local waitconn_bin=""
  waitconn_bin="$(find "$root" -type f -iname 'WaitUntilConnected' 2>/dev/null | head -n 1 || true)"
  if [[ -n "$waitconn_bin" ]]; then
    echo "==> Emu68-tools: installing WaitUntilConnected -> C/"
    local tmp_waitconn="$STAGE/WaitUntilConnected.$$.$RANDOM"
    cp -f "$waitconn_bin" "$tmp_waitconn"
    write_uaem_with_perms "----rwed" "$tmp_waitconn"
    ensure_img_dir "$sys_root/C"
    fs_copy_file "$tmp_waitconn" "$sys_root/C/WaitUntilConnected" --uaemetadata UaeMetafile
  else
    echo "==> WARN: WaitUntilConnected not found in Emu68-tools archive"
  fi

  # Install WiFi firmware files to Devs/Firmware/ (needed by wifipi.device).
  # Emu68-tools stores them under Emu68-WiFi/Devs/Firmware/ rather than Devs/Firmware/.
  local fw_dir=""
  fw_dir="$(find "$root" -type d -iname 'Firmware' -path '*/Devs/*' 2>/dev/null | head -n 1 || true)"
  if [[ -n "$fw_dir" ]]; then
    local fw_count=0
    ensure_img_dir "$sys_root/Devs/Firmware"
    local fw_file
    while IFS= read -r fw_file; do
      [[ -z "$fw_file" ]] && continue
      local fw_name
      fw_name="$(basename "$fw_file")"
      local tmp_fw="$STAGE/fw-${fw_name}.$$.$RANDOM"
      cp -f "$fw_file" "$tmp_fw"
      write_uaem_with_perms "----rwed" "$tmp_fw"
      fs_copy_file "$tmp_fw" "$sys_root/Devs/Firmware/$fw_name" --uaemetadata UaeMetafile
      fw_count=$((fw_count + 1))
    done < <(find "$fw_dir" -maxdepth 1 -type f 2>/dev/null)
    echo "==> Emu68-tools: installed $fw_count WiFi firmware file(s) -> Devs/Firmware/"
  else
    echo "==> WARN: WiFi firmware directory not found in Emu68-tools archive"
  fi

  rm -rf "$tools_extract" 2>/dev/null || true
}

# install_genet_driver()
#   Download genet.device (PiStorm Ethernet) from rondoval/emu68-genet-driver.
#   $1 = sys_root
install_genet_driver() {
  local sys_root="$1"

  echo "==> genet: downloading latest release from rondoval/emu68-genet-driver"

  local api="https://api.github.com/repos/rondoval/emu68-genet-driver/releases/latest"
  local genet_url=""
  genet_url="$(curl -fsSL "$api" \
    | tr -d '\r' \
    | sed -nE 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' \
    | grep -i 'genet\.device' \
    | head -n 1 || true)"

  if [[ -z "$genet_url" ]]; then
    echo "==> WARN: Could not resolve genet.device URL from GitHub API"
    return 0
  fi

  local tmp_genet="$STAGE/genet.device.$$.$RANDOM"
  if ! curl -fL --retry 3 --retry-delay 2 -o "$tmp_genet" "$genet_url"; then
    echo "==> WARN: Failed to download genet.device from $genet_url"
    return 0
  fi

  write_uaem_with_perms "----rwed" "$tmp_genet"
  ensure_img_dir "$sys_root/Devs/Networks"
  fs_copy_file "$tmp_genet" "$sys_root/Devs/Networks/genet.device" --uaemetadata UaeMetafile
  echo "==> genet: installed genet.device -> Devs/Networks/"
}

# --- Disk icon files and icon positioning (Windows parity) --------------------

# hst_icon_update_file()
#   Use hst-amiga to update icon metadata on a temporary host-side .info copy.
#   Returns path to updated temp copy, or original file path on fallback.
#   $1 = src_info
#   $2 = icon_type     (numeric type; e.g. 1=disk)
#   $3 = current_x
#   $4 = current_y
#   $5 = drawer_x
#   $6 = drawer_y
#   $7 = drawer_width
#   $8 = drawer_height
hst_icon_update_file() {
  local src_info="$1"
  local icon_type="${2:-}"
  local current_x="${3:-}"
  local current_y="${4:-}"
  local drawer_x="${5:-}"
  local drawer_y="${6:-}"
  local drawer_width="${7:-}"
  local drawer_height="${8:-}"

  if ! hst_amiga_available; then
    echo "$src_info"
    return 0
  fi

  local tmp_info="$STAGE/icon-$(basename "$src_info").$$.$RANDOM.info"
  if ! cp -f "$src_info" "$tmp_info" 2>/dev/null; then
    echo "$src_info"
    return 0
  fi

  if hst_icon_update_in_place "$tmp_info" "$icon_type" "$current_x" "$current_y" "$drawer_x" "$drawer_y" "$drawer_width" "$drawer_height"; then
    echo "$tmp_info"
    return 0
  fi

  rm -f "$tmp_info" 2>/dev/null || true
  echo "$src_info"
}

write_uaem_with_perms() {
  # Create a UAE metafile (.uaem) sidecar containing Amiga protection bits.
  # Callers must pass --uaemetadata UaeMetafile to fs_copy_file so hst-imager reads this.
  # Format: "<protection_string> <timestamp>"
  local perms="$1"
  local file="$2"
  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S.00')"
  printf "%s %s \n" "$perms" "$ts" > "${file}.uaem"
}

# install_disk_icons()
#   Copy disk.info files for each volume (EMU68BOOT, DH0, DH1+).
#   Preferred source is the selected icon ADF(s); Assets is fallback only.
#   $1 = sys_root  (image path to SYS: / SDH0 root)
install_disk_icons() {
  local sys_root="$1"
  local use_hst_amiga=0
  local icon_root=""
  local icon_source_label=""

  if [[ ${#SELECTED_ICONSET_ADFS[@]} -gt 0 ]]; then
    local icon_adf_root="$STAGE/iconset-icons"
    rm -rf "$icon_adf_root" >/dev/null 2>&1 || true
    mkdir -p "$icon_adf_root"
    for _icon_adf in "${SELECTED_ICONSET_ADFS[@]}"; do
      echo "==> Disk icons: extracting selected icon ADF: $(basename "$_icon_adf")"
      extract_adf_to_host "$_icon_adf" "$icon_adf_root" || true
    done
    if [[ -d "$icon_adf_root" ]]; then
      icon_root="$icon_adf_root"
      icon_source_label="icon ADF"
    fi
  fi

  if [[ -z "$icon_root" ]]; then
    local assets_icon_dir="$ASSETS_DIR/AmigaFiles/IconFiles"
    if [[ -d "$assets_icon_dir" ]]; then
      echo "==> WARN: Disk icons falling back to Assets/AmigaFiles/IconFiles (icon ADF source unavailable)"
      icon_root="$assets_icon_dir"
      icon_source_label="assets fallback"
    fi
  fi

  # If NO icon source available at all, create minimal disk icons for
  # EMU68BOOT and Work using def_drawer.info from the installed Workbench.
  # DH0 keeps whatever icon the OS install media provides.
  if [[ -z "$icon_root" ]]; then
    local def_drawer_tmp="$STAGE/def_drawer_disk.info"
    if fs_pull_file "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info" "$def_drawer_tmp" 2>/dev/null; then
      icon_root="$STAGE/fallback-icons"
      mkdir -p "$icon_root/Emu68BootDisk" "$icon_root/WorkDisk"
      cp "$def_drawer_tmp" "$icon_root/Emu68BootDisk/disk.info"
      cp "$def_drawer_tmp" "$icon_root/WorkDisk/disk.info"
      icon_source_label="def_drawer fallback"
    else
      echo "==> Disk icons: no icon source available (skipping)"
      return 0
    fi
  fi

  echo "==> Installing disk icon files (source: $icon_source_label)"

  # Load per-disk geometry from DiskDefaults.csv (Bash 3.2 compatible: indexed arrays)
  local dd_devnames=() dd_ix=() dd_iy=() dd_dx=() dd_dy=() dd_dw=() dd_dh=()
  local dd_csv
  dd_csv="$(find_input_csv "DiskDefaults.CSV" || true)"
  if [[ -n "$dd_csv" ]]; then
    local _dd_idx=0
    while IFS='|' read -r _dd_dev _dd_x _dd_y _dd_dx _dd_dy _dd_dw _dd_dh; do
      [[ -z "$_dd_dev" ]] && continue
      dd_devnames[$_dd_idx]="$_dd_dev"
      dd_ix[$_dd_idx]="$_dd_x"
      dd_iy[$_dd_idx]="$_dd_y"
      dd_dx[$_dd_idx]="$_dd_dx"
      dd_dy[$_dd_idx]="$_dd_dy"
      dd_dw[$_dd_idx]="$_dd_dw"
      dd_dh[$_dd_idx]="$_dd_dh"
      _dd_idx=$((_dd_idx + 1))
    done < <(awk -F';' '
      function trim(s){ gsub(/\r/, "", s); sub(/^[ \t"]+/, "", s); sub(/[ \t"]+$/, "", s); return s }
      function norm(s){ s=tolower(trim(s)); gsub(/[^a-z0-9]/, "", s); return s }
      function get(k,   idx,val){ idx=h[k]; if (idx == 0) return ""; val=trim($(idx)); return val }
      NR==1{ for(i=1;i<=NF;i++) h[norm($i)] = i; next }
      {
        dev=get("devicename"); if (dev == "") next
        print dev "|" get("iconx") "|" get("icony") "|" get("drawerx") "|" get("drawery") "|" get("dwidth") "|" get("dheight")
      }
    ' "$dd_csv")
    echo "==> DiskDefaults: loaded ${#dd_devnames[@]} entries from $(basename "$dd_csv")"
  fi

  # lookup_disk_defaults <DeviceName> — sets dd_* result vars
  # Returns 0 if found, 1 if not (caller uses fallback values).
  local dd_result_ix dd_result_iy dd_result_dx dd_result_dy dd_result_dw dd_result_dh
  lookup_disk_defaults() {
    local target="$1"
    local _i
    for _i in $(seq 0 $((${#dd_devnames[@]} - 1))); do
      if [[ "${dd_devnames[$_i]}" == "$target" ]]; then
        dd_result_ix="${dd_ix[$_i]}"
        dd_result_iy="${dd_iy[$_i]}"
        dd_result_dx="${dd_dx[$_i]}"
        dd_result_dy="${dd_dy[$_i]}"
        dd_result_dw="${dd_dw[$_i]}"
        dd_result_dh="${dd_dh[$_i]}"
        return 0
      fi
    done
    return 1
  }

  if [[ "$APPLY_ICON_POSITIONS" == "1" ]]; then
    if hst_amiga_available; then
      use_hst_amiga=1
      echo "==> Icon positioning: applying disk icon geometry via hst-amiga (DiskDefaults.csv)"
    else
      echo "==> Icon positioning: keeping icon geometry from source icons"
    fi
  else
    if hst_amiga_available; then
      use_hst_amiga=1
      echo "==> Disk icons: applying default left-column positions"
    else
      echo "==> Disk icons: keeping icon geometry from source icons"
    fi
  fi

  find_disk_icon_for_role() {
    local role="$1"
    local root="$2"
    local cand=""

    # Windows reference (IconSets CSV): GlowIcons ADF uses Disk.Info (root) for
    # System partition, and Prefs/Env-Archive/Sys/def_harddisk.info for Work and
    # EMU68BOOT partitions.  Standard 4-color icons use the same icon for all.
    # We search for def_harddisk.info first for non-System roles so that GlowIcons
    # automatically get the hard-drive icon while standard icons fall through to
    # the generic Disk.info at the ADF root.
    case "$role" in
      sd0)
        for cand in \
          "$root/SD0_Disk.info" \
          "$root/Emu68BootDisk/disk.info" \
          "$root/SD0/Disk.info"
        do
          [[ -f "$cand" ]] && { echo "$cand"; return 0; }
        done
        # GlowIcons: use def_harddisk.info (hard drive icon, not system floppy)
        cand="$(find "$root" -type f -iname 'def_harddisk.info' 2>/dev/null | head -n1 || true)"
        [[ -n "$cand" && -f "$cand" ]] && { echo "$cand"; return 0; }
        # Wildcard search
        cand="$(find "$root" -type f \( -iname '*sd0*disk.info' -o -iname '*emu68*boot*disk.info' -o -iname '*boot*disk.info' \) 2>/dev/null | head -n1 || true)"
        [[ -n "$cand" && -f "$cand" ]] && { echo "$cand"; return 0; }
        # Final fallback: generic Disk.info from ADF root (same as System)
        [[ -f "$root/Disk.info" ]] && { echo "$root/Disk.info"; return 0; }
        ;;
      dh0)
        for cand in \
          "$root/DH0_Disk.info" \
          "$root/SystemDisk/disk.info" \
          "$root/DH0/Disk.info" \
          "$root/Disk.info"
        do
          [[ -f "$cand" ]] && { echo "$cand"; return 0; }
        done
        cand="$(find "$root" -type f \( -iname '*dh0*disk.info' -o -iname '*system*disk.info' \) 2>/dev/null | head -n1 || true)"
        ;;
      dh1)
        for cand in \
          "$root/DH1_Disk.info" \
          "$root/WorkDisk/disk.info" \
          "$root/DH1/Disk.info"
        do
          [[ -f "$cand" ]] && { echo "$cand"; return 0; }
        done
        # GlowIcons: use def_harddisk.info (hard drive icon, not system floppy)
        cand="$(find "$root" -type f -iname 'def_harddisk.info' 2>/dev/null | head -n1 || true)"
        [[ -n "$cand" && -f "$cand" ]] && { echo "$cand"; return 0; }
        # Wildcard search
        cand="$(find "$root" -type f \( -iname '*dh1*disk.info' -o -iname '*work*disk.info' \) 2>/dev/null | head -n1 || true)"
        [[ -n "$cand" && -f "$cand" ]] && { echo "$cand"; return 0; }
        # Final fallback: generic Disk.info from ADF root (same as System)
        [[ -f "$root/Disk.info" ]] && { echo "$root/Disk.info"; return 0; }
        ;;
    esac

    if [[ -n "$cand" && -f "$cand" ]]; then
      echo "$cand"
      return 0
    fi
    return 1
  }

  # Per-disk positioning from DiskDefaults.csv with hardcoded fallbacks

  # EMU68BOOT (SD0:) - FAT partition
  local sd0_ix=15 sd0_iy=60 sd0_dx=50 sd0_dy=50 sd0_dw=500 sd0_dh=160
  if [[ "$APPLY_ICON_POSITIONS" == "1" ]] && lookup_disk_defaults "SD0"; then
    sd0_ix="$dd_result_ix"; sd0_iy="$dd_result_iy"
    sd0_dx="$dd_result_dx"; sd0_dy="$dd_result_dy"
    sd0_dw="$dd_result_dw"; sd0_dh="$dd_result_dh"
  fi
  local sd0_icon=""
  sd0_icon="$(find_disk_icon_for_role sd0 "$icon_root" || true)"
  if [[ -n "$sd0_icon" ]]; then
    local sd0_icon_to_copy="$sd0_icon"
    if [[ "$use_hst_amiga" -eq 1 ]]; then
      sd0_icon_to_copy="$(hst_icon_update_file "$sd0_icon" "1" "$sd0_ix" "$sd0_iy" "$sd0_dx" "$sd0_dy" "$sd0_dw" "$sd0_dh")"
    fi
    fs_copy_file "$sd0_icon_to_copy" "$FAT_ROOT/Disk.info"
    echo "  -> SD0 (EMU68BOOT): $(basename "$sd0_icon") @ ${sd0_ix},${sd0_iy}"
  fi

  # DH0 (System) - first Amiga partition
  local dh0_ix=15 dh0_iy=4 dh0_dx=50 dh0_dy=50 dh0_dw=533 dh0_dh=160
  if [[ "$APPLY_ICON_POSITIONS" == "1" ]] && lookup_disk_defaults "SDH0"; then
    dh0_ix="$dd_result_ix"; dh0_iy="$dd_result_iy"
    dh0_dx="$dd_result_dx"; dh0_dy="$dd_result_dy"
    dh0_dw="$dd_result_dw"; dh0_dh="$dd_result_dh"
  fi
  local dh0_icon=""
  dh0_icon="$(find_disk_icon_for_role dh0 "$icon_root" || true)"
  if [[ -n "$dh0_icon" ]]; then
    local dh0_icon_to_copy="$dh0_icon"
    if [[ "$use_hst_amiga" -eq 1 ]]; then
      dh0_icon_to_copy="$(hst_icon_update_file "$dh0_icon" "1" "$dh0_ix" "$dh0_iy" "$dh0_dx" "$dh0_dy" "$dh0_dw" "$dh0_dh")"
    fi
    fs_copy_file "$dh0_icon_to_copy" "$sys_root/Disk.info"
    echo "  -> ${PART_NAMES[0]} (System): $(basename "$dh0_icon") @ ${dh0_ix},${dh0_iy}"
  fi

  # DH1+ (Work partitions) - use work disk icon for all non-boot partitions
  local work_icon=""
  work_icon="$(find_disk_icon_for_role dh1 "$icon_root" || true)"
  if [[ -n "$work_icon" ]]; then
    # DH1 defaults from CSV; DH2+ extrapolate y += 56 from previous
    local prev_work_y=116
    local _pi
    for _pi in $(seq 1 $((NUM_PARTS - 1))); do
      if [[ "${PART_BYTES[$_pi]:-0}" -gt 0 && -n "${PART_PNS[$_pi]:-}" ]]; then
        local part_root="$IMG/mbr/2/rdb/${PART_PNS[$_pi]}"
        # Skip if partition doesn't exist in image
        if ! "$HST_IMAGER" fs dir "$part_root" >/dev/null 2>&1; then
          continue
        fi

        local wk_ix=15 wk_iy="$prev_work_y" wk_dx=50 wk_dy=50 wk_dw=500 wk_dh=160
        local device_name="SDH${_pi}"
        if [[ "$APPLY_ICON_POSITIONS" == "1" ]] && lookup_disk_defaults "$device_name"; then
          wk_ix="$dd_result_ix"; wk_iy="$dd_result_iy"
          wk_dx="$dd_result_dx"; wk_dy="$dd_result_dy"
          wk_dw="$dd_result_dw"; wk_dh="$dd_result_dh"
        fi

        local work_icon_to_copy="$work_icon"
        if [[ "$use_hst_amiga" -eq 1 ]]; then
          work_icon_to_copy="$(hst_icon_update_file "$work_icon" "1" "$wk_ix" "$wk_iy" "$wk_dx" "$wk_dy" "$wk_dw" "$wk_dh")"
        fi
        fs_copy_file "$work_icon_to_copy" "$part_root/Disk.info"
        echo "  -> ${PART_NAMES[$_pi]} (${PART_LABELS[$_pi]}): $(basename "$work_icon") @ ${wk_ix},${wk_iy}"
        prev_work_y=$((wk_iy + 56))
      fi
    done
  fi
}

ensure_transfer_drawer_icon() {
  local transfer_root="$1"

  # Only act if the Transfer drawer exists in this partition.
  if ! "$HST_IMAGER" fs dir "$transfer_root/Transfer" >/dev/null 2>&1; then
    return 0
  fi

  # Ensure the Transfer drawer is visible on Workbench (requires an .info icon).
  if "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info" >/dev/null 2>&1; then
    "$HST_IMAGER" fs copy "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info" "$transfer_root/Transfer.info" --force >/dev/null 2>&1 || true
  elif "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Env-Archive/Sys/Def_Drawer.info" >/dev/null 2>&1; then
    "$HST_IMAGER" fs copy "$SYS_ROOT/Prefs/Env-Archive/Sys/Def_Drawer.info" "$transfer_root/Transfer.info" --force >/dev/null 2>&1 || true
  elif "$HST_IMAGER" fs dir "$SYS_ROOT/Tools.info" >/dev/null 2>&1; then
    "$HST_IMAGER" fs copy "$SYS_ROOT/Tools.info" "$transfer_root/Transfer.info" --force >/dev/null 2>&1 || true
  elif "$HST_IMAGER" fs dir "$SYS_ROOT/System.info" >/dev/null 2>&1; then
    "$HST_IMAGER" fs copy "$SYS_ROOT/System.info" "$transfer_root/Transfer.info" --force >/dev/null 2>&1 || true
  elif "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs.info" >/dev/null 2>&1; then
    "$HST_IMAGER" fs copy "$SYS_ROOT/Prefs.info" "$transfer_root/Transfer.info" --force >/dev/null 2>&1 || true
  fi
}

ensure_drawer_visible_in_root() {
  local sys_root="$1"
  local drawer_name="$2"
  local drawer_path="$sys_root/$drawer_name"

  # Only act when the drawer exists but has no .info icon.
  if ! "$HST_IMAGER" fs dir "$drawer_path" >/dev/null 2>&1; then
    return 0
  fi
  if "$HST_IMAGER" fs dir "$drawer_path.info" >/dev/null 2>&1; then
    return 0
  fi

  local icon_src=""
  local cand
  for cand in \
    "$sys_root/Prefs/Env-Archive/Sys/def_drawer.info" \
    "$sys_root/Prefs/Env-Archive/Sys/Def_Drawer.info" \
    "$sys_root/System.info" \
    "$sys_root/Tools.info"; do
    if "$HST_IMAGER" fs dir "$cand" >/dev/null 2>&1; then
      icon_src="$cand"
      break
    fi
  done

  if [[ -n "$icon_src" ]]; then
    "$HST_IMAGER" fs copy "$icon_src" "$drawer_path.info" --force >/dev/null 2>&1 || true
  fi
}

ensure_fat95_in_sys() {
  local sys_root="$1"

  # Already present as a file?
  local tmp_check="$STAGE/.check-fat95.$$.$RANDOM"
  if fs_pull_file "$sys_root/L/fat95" "$tmp_check" >/dev/null 2>&1 || \
     fs_pull_file "$sys_root/L/FAT95" "$tmp_check" >/dev/null 2>&1; then
    rm -f "$tmp_check" >/dev/null 2>&1 || true
    return 0
  fi
  rm -f "$tmp_check" >/dev/null 2>&1 || true

  ensure_img_dir "$sys_root/L"

  # 1) Check Assets for a direct fat95 binary (skip in download-only mode)
  if [[ "$DOWNLOAD_ONLY" != "1" ]]; then
    for cand in \
      "$ASSETS_DIR/AmigaFiles/L/fat95" \
      "$ASSETS_DIR/AmigaFiles/L/FAT95" \
      "$ASSETS_DIR/AmigaFiles/System/L/fat95" \
      "$ASSETS_DIR/AmigaFiles/System/L/FAT95"; do
      if [[ -f "$cand" ]]; then
        echo "==> EMU68BOOT: installing fat95 from Assets: $cand"
        fs_copy_file "$cand" "$sys_root/L/fat95"
        return 0
      fi
    done
  fi

  # 2) Check Assets for fat95.lha (skip in download-only mode)
  local fat95_lha=""
  if [[ "$DOWNLOAD_ONLY" != "1" ]]; then
    for cand in \
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/fat95.lha" \
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/FAT95.LHA" \
      "$ASSETS_DIR/AmigaFiles/LocalAmigaPackages/Fat95.lha"; do
      [[ -f "$cand" ]] && fat95_lha="$cand" && break
    done
  fi

  # 3) Download from Aminet if not in Assets
  if [[ -z "$fat95_lha" ]]; then
    local url="http://aminet.net/pub/aminet/disk/misc/fat95.lha"
    fat95_lha="$STAGE/fat95.lha"
    echo "==> EMU68BOOT: downloading fat95.lha from Aminet"
    curl -fL --retry 3 --retry-delay 2 -o "$fat95_lha" "$url"
  fi

  # Extract and copy fat95 into SYS:L
  local tmp_fat="$STAGE/fat95-extract"
  rm -rf "$tmp_fat" >/dev/null 2>&1 || true
  mkdir -p "$tmp_fat"
  "$HST_IMAGER" fs extract "$fat95_lha" "$tmp_fat" --recursive --force >/dev/null 2>&1 || \
  "$HST_IMAGER" fs extract "$fat95_lha" "$tmp_fat" --recursive >/dev/null 2>&1 || true

  local fat_bin=""
  fat_bin="$(find "$tmp_fat" -type f \( -path '*/l/fat95' -o -path '*/L/FAT95' -o -name 'fat95' -o -name 'FAT95' \) 2>/dev/null | head -n1 || true)"
  if [[ -z "$fat_bin" ]]; then
    fat_bin="$(find "$tmp_fat" -type f -iname '*fat95' 2>/dev/null | head -n1 || true)"
  fi
  if [[ -n "$fat_bin" ]]; then
    echo "==> EMU68BOOT: installing fat95 -> SYS:L/fat95"
    fs_copy_file "$fat_bin" "$sys_root/L/fat95"
    if "$HST_IMAGER" fs dir "$sys_root/L/fat95" >/dev/null 2>&1; then
      echo "==> EMU68BOOT: fat95 installed in SYS:L"
    fi
    return 0
  fi

  echo "==> WARN: fat95 not found after extraction; EMU68BOOT may not mount." >&2
  return 1
}

# --- Binary prefs patching (Windows parity) -----------------------------------

# write_screenmode_prefs()
#   Patch a template ScreenMode.prefs binary with the selected ModeID and color depth,
#   then copy to SYS:Prefs/ScreenMode.prefs on the image.
#   $1 = sys_root  (image path to SYS:)
#   $2 = mode_id   (8-char hex string, e.g. "50AF1101")
#   $3 = depth_hex  (2-char hex string, e.g. "08")
write_screenmode_prefs() {
  local sys_root="$1"
  local mode_id="$2"
  local depth_hex="$3"

  if [[ -z "$mode_id" || -z "$depth_hex" ]]; then
    echo "==> ScreenMode.prefs: skipping (no ModeID or depth specified)"
    return 0
  fi

  # Look for template in Assets (check Env-Archive/Sys first — that's where it lives)
  local template=""
  for cand in \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Env-Archive/Sys/screenmode.prefs" \
    "$ASSETS_DIR/AmigaFiles/PrefsTemplates/ScreenMode.prefs" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/ScreenMode.prefs"; do
    if [[ -f "$cand" ]]; then
      template="$cand"
      break
    fi
  done

  if [[ -z "$template" ]]; then
    echo "==> ScreenMode.prefs: template not found in Assets (skipping binary patching)"
    return 0
  fi

  local tmp_prefs="$STAGE/screenmode.prefs"
  cp "$template" "$tmp_prefs"

  # Patch ModeID at offset 50 (4 bytes, big-endian)
  local b0="${mode_id:0:2}" b1="${mode_id:2:2}" b2="${mode_id:4:2}" b3="${mode_id:6:2}"
  printf "\\x${b0}\\x${b1}\\x${b2}\\x${b3}" | \
    dd of="$tmp_prefs" bs=1 seek=50 count=4 conv=notrunc 2>/dev/null

  # Patch color depth at offset 59 (1 byte)
  printf "\\x${depth_hex}" | \
    dd of="$tmp_prefs" bs=1 seek=59 count=1 conv=notrunc 2>/dev/null

  # Write RTG to active screenmode.prefs + .User; keep native in .Native.
  # IPrefs (relocated after LoadWB via `Run >NIL: IPrefs`) reads the active
  # prefs and applies RTG. EndCLI closes the shell before IPrefs processes
  # the mode switch → no blocking window → no Intuition requester.
  #
  #   screenmode.prefs        = RTG user mode    (IPrefs applies RTG directly)
  #   screenmode.prefs.User   = RTG user mode    (UAEGFX UAE branch / rexx rename)
  #   screenmode.prefs.Native = NATIVE template  (UAE/Videocore fallback)
  #   Prefs/screenmode.prefs  = RTG user mode    (Prefs drawer copy)
  ensure_img_dir "$sys_root/Prefs/Env-Archive/Sys"
  fs_copy_file "$tmp_prefs" "$sys_root/Prefs/Env-Archive/Sys/screenmode.prefs.User"
  fs_copy_file "$tmp_prefs" "$sys_root/Prefs/Env-Archive/Sys/screenmode.prefs"
  fs_copy_file "$template"  "$sys_root/Prefs/Env-Archive/Sys/screenmode.prefs.Native"
  ensure_img_dir "$sys_root/Prefs"
  fs_copy_file "$tmp_prefs" "$sys_root/Prefs/screenmode.prefs"

  # Write ScreenModeChipset environment variable for CheckScreenModeandChipset.rexx
  # Native chipset modes use $0001/$0002 prefix (PAL/NTSC) = "OCS"
  # Everything else is RTG (VideoCore board prefixes $50xx)
  local chipset_type="RTG"
  local mode_prefix="${mode_id:0:4}"
  case "$mode_prefix" in
    0001|0002) chipset_type="OCS" ;;  # PAL/NTSC native modes
    *) chipset_type="RTG" ;;          # VideoCore RTG (board prefix varies by resolution)
  esac

  local tmp_chipset="$STAGE/ScreenModeChipset"
  printf "%s\n" "$chipset_type" > "$tmp_chipset"
  make_lf_only "$tmp_chipset"
  fs_copy_file "$tmp_chipset" "$sys_root/Prefs/Env-Archive/ScreenModeChipset"
  echo "==> Patched screenmode.prefs + .User (ModeID=0x${mode_id}, depth=0x${depth_hex}, chipset=$chipset_type)"
  echo "==> screenmode.prefs.Native left as native template (UAE Videocore fallback)"
}

# write_wbconfig_prefs()
#   Patch WBConfig.prefs byte 51 to enable/disable Workbench backdrop.
#   PC reference parity: Get-BackdropPrefs.ps1 patches offset 51 with 0x01/0x00.
#   The file already exists on the image from the AmigaOS installation.
#   $1 = sys_root  (image path to SYS:)
#   $2 = backdrop  ("1" = enabled, "0" = disabled)
write_wbconfig_prefs() {
  local sys_root="$1"
  local backdrop="${2:-1}"

  local prefs_path="Prefs/Env-Archive/Sys/WBConfig.prefs"
  local tmp_prefs="$STAGE/WBConfig.prefs"

  # Pull existing WBConfig.prefs from image (written by AmigaOS ADF/ISO extraction)
  if ! fs_pull_file "$sys_root/$prefs_path" "$tmp_prefs" >/dev/null 2>&1; then
    echo "==> WARN: WBConfig.prefs not found on image — cannot set backdrop"
    return 0
  fi

  local file_size
  file_size="$(wc -c < "$tmp_prefs" | tr -d ' ')"
  if [[ "$file_size" -lt 52 ]]; then
    echo "==> WARN: WBConfig.prefs too small (${file_size} bytes) — cannot patch backdrop"
    return 0
  fi

  # Patch byte 51 (0-indexed): 0x01 = backdrop on, 0x00 = backdrop off
  local byte_val="00"
  [[ "$backdrop" == "1" ]] && byte_val="01"
  printf "\\x${byte_val}" | dd of="$tmp_prefs" bs=1 seek=51 count=1 conv=notrunc 2>/dev/null

  # Write patched file back to image
  fs_copy_file "$tmp_prefs" "$sys_root/$prefs_path"
  echo "==> Patched WBConfig.prefs byte 51 = 0x${byte_val} (backdrop=$([ "$backdrop" = "1" ] && echo enabled || echo disabled))"
}

# locale_to_keymap()
#   Map a locale code (e.g. "de") to an AmigaOS keymap name (e.g. "d").
#   Returns empty string for English or unknown locales (keep default usa1).
locale_to_keymap() {
  case "$(printf "%s" "$1" | tr '[:upper:]' '[:lower:]')" in
    de) echo "d" ;;
    fr) echo "f" ;;
    it) echo "i" ;;
    nl) echo "n" ;;
    dk) echo "dk" ;;
    es) echo "e" ;;
    se) echo "s" ;;
    uk|gb) echo "gb" ;;
    *) echo "" ;;
  esac
}

# write_keymap_prefs()
#   Patch input.prefs to use a specific keyboard layout (keymap).
#   The keymap name is stored in the IFF PREF/INPT chunk as a length-prefixed
#   string. We use Python to safely patch the binary structure.
#   $1 = sys_root   $2 = keymap_name (e.g. "d", "f", "gb")
write_keymap_prefs() {
  local sys_root="$1"
  local keymap_name="$2"

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

  local prefs_path="Prefs/Env-Archive/Sys/input.prefs"
  local tmp_prefs="$STAGE/input.prefs"

  if ! fs_pull_file "$sys_root/$prefs_path" "$tmp_prefs" >/dev/null 2>&1; then
    echo "==> WARN: input.prefs not found — cannot set keymap"
    return 0
  fi

  if ! command -v python3 >/dev/null 2>&1; then
    echo "==> WARN: python3 not found — cannot patch keymap in input.prefs"
    return 0
  fi

  python3 - "$tmp_prefs" "$keymap_name" <<'PYEOF'
import struct, sys
prefs_path, new_keymap = sys.argv[1], sys.argv[2].encode("ascii")
data = bytearray(open(prefs_path, "rb").read())
# Find INPT chunk in IFF PREF file
pos = data.find(b"INPT")
if pos < 0:
    print("==> WARN: INPT chunk not found in input.prefs", file=sys.stderr)
    sys.exit(1)
cs_off = pos + 4  # chunk size offset
old_cs = struct.unpack_from(">I", data, cs_off)[0]
cd_off = cs_off + 4  # chunk data offset
# Read old keymap: 2-byte BE length, then name bytes (padded to even)
old_len = struct.unpack_from(">H", data, cd_off)[0]
old_padded = old_len + (old_len % 2)
# Build new keymap field
new_len = len(new_keymap)
new_padded = new_len + (new_len % 2)
new_field = struct.pack(">H", new_len) + new_keymap + b"\x00" * (new_padded - new_len)
old_field_size = 2 + old_padded
# Splice: replace old keymap field with new one
new_data = data[:cd_off] + new_field + data[cd_off + old_field_size:]
# Update INPT chunk size
new_cs = old_cs - old_field_size + len(new_field)
struct.pack_into(">I", new_data, cs_off, new_cs)
# Update FORM size (offset 4 in file)
struct.pack_into(">I", new_data, 4, len(new_data) - 8)
open(prefs_path, "wb").write(new_data)
old_name = data[cd_off+2:cd_off+2+old_len].decode("ascii", errors="replace")
print(f"==> Patched input.prefs keymap: \"{old_name}\" -> \"{new_keymap.decode()}\"")
PYEOF

  if [[ $? -ne 0 ]]; then
    echo "==> WARN: Failed to patch input.prefs keymap"
    return 0
  fi

  fs_copy_file "$tmp_prefs" "$sys_root/$prefs_path"
}

# --- Phase 2: Screen mode, Unicam, WiFi config entries in config.txt ---------
write_phase2_config() {
  local fat_root="$1"
  local tmp_cfg="$STAGE/config.txt"
  local tmp_inc="$STAGE/emu68imager.cfg"

  # Pull current config.txt (do not clobber upstream Emu68 config)
  if ! fs_pull_file "$fat_root/config.txt" "$tmp_cfg"; then
    printf "initramfs kick.rom\n" > "$tmp_cfg"
  fi

  # Only do global initramfs replacement when NOT using the Assets template.
  # The template has initramfs lines inside GPIO-conditional sections; a global
  # replacement would break stealth mode and PiStorm variant detection.
  if [[ "${USED_ASSETS_CONFIG_TEMPLATE:-0}" -ne 1 ]]; then
    if grep -qiE '^[[:space:]]*initramfs[[:space:]]+' "$tmp_cfg"; then
      perl -pi -e 's/^[ \t]*initramfs[ \t]+.*/initramfs kick.rom/i' "$tmp_cfg"
    else
      printf "\ninitramfs kick.rom\n" >> "$tmp_cfg"
    fi
  fi

  # Defensive: comment out a known-bad corrupted line seen in some builds
  perl -pi -e 's/^[ \t]*r now\./# r now./' "$tmp_cfg" 2>/dev/null || true

  # Ensure config includes our separate file (safe injection, no clobber)
  if ! grep -qiE '^[[:space:]]*include[[:space:]]+emu68imager\.cfg' "$tmp_cfg"; then
    printf "\n[all]\n# Amiga-Imager (generated)\ninclude emu68imager.cfg\n" >> "$tmp_cfg"
  fi

  # Build emu68imager.cfg (only our settings)
  : > "$tmp_inc"
  printf "# Amiga Imager generated settings\n" >> "$tmp_inc"

  # Screen mode: handled entirely through AmigaOS-side Picasso96 prefs
  # (screenmode.prefs.User + Picasso96Settings). CaffeineOS (known-working)
  # does NOT use setvar for display config — Emu68 auto-configures VideoCore.
  # Using setvar ScreenWidth/ScreenHeight/WBDepth caused Emu68 to hang before
  # Kickstart execution on PiStorm32lite + CM4.
  if [[ -n "$SCREEN_WIDTH" && -n "$SCREEN_HEIGHT" ]]; then
    echo "==> Phase 2: Screen mode ${SCREEN_WIDTH}x${SCREEN_HEIGHT}@${SCREEN_FPS:-60} (AmigaOS-side prefs only, no setvar)"
  fi

  # Unicam
  if [[ "${UNICAM_ENABLE:-}" == "1" ]]; then
    echo "==> Phase 2: Writing Unicam config"
    printf "setvar Unicam 1\n" >> "$tmp_inc"
    [[ -n "${UNICAM_AUTOSTART:-}" ]] && printf "setvar UnicamAutostart %s\n" "$UNICAM_AUTOSTART" >> "$tmp_inc"
    [[ -n "${UNICAM_SCALING:-}" ]] && printf "setvar UnicamScaling %s\n" "$UNICAM_SCALING" >> "$tmp_inc"
    [[ -n "${UNICAM_BRIGHTNESS:-}" ]] && printf "setvar UnicamBrightness %s\n" "$UNICAM_BRIGHTNESS" >> "$tmp_inc"
    [[ -n "${UNICAM_CONTRAST:-}" ]] && printf "setvar UnicamContrast %s\n" "$UNICAM_CONTRAST" >> "$tmp_inc"
    [[ -n "${UNICAM_SIZEW:-}" ]] && printf "setvar UnicamSizeW %s\n" "$UNICAM_SIZEW" >> "$tmp_inc"
    [[ -n "${UNICAM_SIZEH:-}" ]] && printf "setvar UnicamSizeH %s\n" "$UNICAM_SIZEH" >> "$tmp_inc"
    [[ -n "${UNICAM_OFFSETX:-}" ]] && printf "setvar UnicamOffsetX %s\n" "$UNICAM_OFFSETX" >> "$tmp_inc"
    [[ -n "${UNICAM_OFFSETY:-}" ]] && printf "setvar UnicamOffsetY %s\n" "$UNICAM_OFFSETY" >> "$tmp_inc"
  fi

  make_lf_only "$tmp_cfg"
  make_lf_only "$tmp_inc"
  fs_copy_file "$tmp_cfg" "$fat_root/config.txt"
  fs_copy_file "$tmp_inc" "$fat_root/emu68imager.cfg"

  # WiFi: write wpa_supplicant.conf to FAT partition if SSID is set.
  # NOTE: Emu68 firmware does not use this file — it is kept for compatibility
  # with Pi OS setups that read wpa_supplicant.conf from the boot partition.
  if [[ -n "$WIFI_SSID" ]]; then
    echo "==> Phase 2: Writing WiFi config (SSID: $WIFI_SSID)"
    local _esc_ssid _esc_pass
    _esc_ssid="$(escape_for_dquote "$WIFI_SSID")"
    _esc_pass="$(escape_for_dquote "${WIFI_PASSWORD:-}")"
    local tmp_wifi="$STAGE/wpa_supplicant.conf"
    if [[ -n "${WIFI_PASSWORD:-}" ]]; then
      cat > "$tmp_wifi" <<WPAEOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=US

network={
    ssid="$_esc_ssid"
    scan_ssid=1
    key_mgmt=WPA-PSK
    proto=RSN WPA
    pairwise=CCMP TKIP
    psk="$_esc_pass"
}
WPAEOF
    else
      cat > "$tmp_wifi" <<WPAEOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=US

network={
    ssid="$_esc_ssid"
    scan_ssid=1
    key_mgmt=NONE
}
WPAEOF
    fi
    make_lf_only "$tmp_wifi"
    fs_copy_file "$tmp_wifi" "$fat_root/wpa_supplicant.conf"
  fi

}

# configure_amiga_network()
#   Configure Amiga-side networking (Roadshow) including WiFi.
#   Creates device prefs files and wireless.prefs.
#   $1 = sys_root
configure_amiga_network() {
  local sys_root="$1"

  [[ -n "${NETWORK_STACK:-}" && "$NETWORK_STACK" != "none" ]] || return 0

  echo "==> Configuring Amiga network (stack: $NETWORK_STACK)"

  # Write genet device prefs for the selected network stack
  local tmp_genet_prefs="$STAGE/genet.prefs"
  cat > "$tmp_genet_prefs" <<'GENETPREFS'
UNIT_TASK_PRIORITY=5
UNIT_STACK_SIZE=65536
USE_DMA=0
USE_MIAMI_WORKAROUND=0
BUDGET=32
PERIODIC_TASK_MS=200
RX_COALESCE_USECS=500
RX_COALESCE_FRAMES=10
TX_COALESCE_FRAMES=10
GENETPREFS

  make_lf_only "$tmp_genet_prefs"

  ensure_img_dir "$sys_root/Prefs/Env-Archive"
  fs_copy_file "$tmp_genet_prefs" "$sys_root/Prefs/Env-Archive/genet_Roadshow.prefs"
  echo "  -> Created genet_Roadshow.prefs"

  # NOTE: Do NOT create S:Network-Startup.
  # The Windows reference does not auto-connect at boot. Network connection is
  # initiated manually via Roadie GUI ("Connect" button) or the PiStorm Online
  # script. Creating a Network-Startup that runs AddNetInterface at boot causes
  # "Object exists" errors when Roadie later tries to add the same interface.
  # The User-Startup_Roadshow overlay's "IF EXISTS S:Network-Startup" check
  # will be a no-op without this file, which is the intended behavior.
  if [[ "$NETWORK_STACK" == "roadshow" ]]; then
    if [[ -n "${WIFI_SSID:-}" ]]; then
      echo "  -> Roadshow WiFi (wifipi): connect via Roadie or PiStorm Online"

      # Create wireless.prefs for the Amiga side (used by wirelessmanager).
      # The PiStorm Online script runs: wirelessmanager CONFIG="ENVARC:Sys/Wireless.prefs"
      # Without this file, wirelessmanager can't associate with the WiFi AP and
      # AddNetInterface wifipi will time out.
      # WirelessManager (prism2v2) is a wpa_supplicant port. It DOES parse key_mgmt.
      # For WPA networks: key_mgmt=WPA-PSK prevents fallback to open-network scanning.
      # For open networks: key_mgmt=NONE explicitly selects open mode.
      # Matches the format generated by the "Wifi Config" Amiga tool.
      local _esc_ssid_a _esc_pass_a
      _esc_ssid_a="$(escape_for_dquote "$WIFI_SSID")"
      _esc_pass_a="$(escape_for_dquote "${WIFI_PASSWORD:-}")"
      local tmp_wprefs="$STAGE/wireless.prefs"
      if [[ -n "${WIFI_PASSWORD:-}" ]]; then
        cat > "$tmp_wprefs" <<WPEOF
network={
   ssid="$_esc_ssid_a"
   scan_ssid=1
   key_mgmt=WPA-PSK
   proto=RSN WPA
   pairwise=CCMP TKIP
   psk="$_esc_pass_a"
}
WPEOF
      else
        cat > "$tmp_wprefs" <<WPEOF
network={
   ssid="$_esc_ssid_a"
   scan_ssid=1
   key_mgmt=NONE
}
WPEOF
      fi
      make_lf_only "$tmp_wprefs"
      ensure_img_dir "$sys_root/Prefs/Env-Archive/Sys"
      fs_copy_file "$tmp_wprefs" "$sys_root/Prefs/Env-Archive/Sys/wireless.prefs"
      echo "  -> Created ENVARC:Sys/wireless.prefs for wirelessmanager"
    else
      echo "  -> Roadshow Ethernet (genet): connect via Roadie or PiStorm Online"
    fi

    # Roadshow TCP/UDP buffer tuning — larger buffers improve throughput on PiStorm.
    # Roadshow reads these from ENVARC:roadshow/tcp/ and ENVARC:roadshow/udp/.
    local _rs_dir="$STAGE/roadshow-buffers"
    mkdir -p "$_rs_dir/tcp" "$_rs_dir/udp"
    printf '220000' > "$_rs_dir/tcp/recvspace"
    printf '32768'  > "$_rs_dir/tcp/sendspace"
    printf '220000' > "$_rs_dir/udp/recvspace"
    printf '32768'  > "$_rs_dir/udp/sendspace"
    ensure_img_dir "$sys_root/Prefs/Env-Archive/roadshow/tcp"
    ensure_img_dir "$sys_root/Prefs/Env-Archive/roadshow/udp"
    fs_copy_file "$_rs_dir/tcp/recvspace" "$sys_root/Prefs/Env-Archive/roadshow/tcp/recvspace"
    fs_copy_file "$_rs_dir/tcp/sendspace" "$sys_root/Prefs/Env-Archive/roadshow/tcp/sendspace"
    fs_copy_file "$_rs_dir/udp/recvspace" "$sys_root/Prefs/Env-Archive/roadshow/udp/recvspace"
    fs_copy_file "$_rs_dir/udp/sendspace" "$sys_root/Prefs/Env-Archive/roadshow/udp/sendspace"
    echo "  -> Installed Roadshow TCP/UDP buffer tuning to ENVARC:roadshow/"
  fi
}

write_phase2_config "$FAT_ROOT"

# -----------------------------------------------------------------------------
# 5) Populate SYS: from ADFs (Windows-style merge)
# -----------------------------------------------------------------------------
echo "==> 5) Populate SYS: from ADFs (Windows-style merge)"

detect_locale_code_from_adf_name() {
  # Return a normalized 2-letter locale code (en,de,fr,...) for an ADF filename.
  # Accepts variants like:
  #   Locale-DE.adf, LocaleDE3_2.adf, locale_de.adf, locale-german.adf
  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 ;;
  esac

  # Handle versioned names like "Locale3.2.3-DE" (language code may appear later).
  for _code in en de fr it nl uk dk es se; 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() {
  # Locale base disks sometimes have no language code (e.g. Locale.adf)
  # or only a version suffix (e.g. Locale3.2.3.adf). These should always
  # be included even when locale filtering is active, because they contain
  # shared Country definitions.
  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

# detect_workbench_version()
#   Parse Workbench version from ADF basename and set DETECTED_WB_VERSION.
#   Preserves exact version (e.g. 3.2.3, 3.2.2.1) for accurate logging.
#   Defaults to 3.2 if undetectable.
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

  # Strip extension and "Workbench" prefix to isolate version string
  # e.g. "Workbench3.2.adf" -> "3.2", "Workbench3.2.2.1.adf" -> "3.2.2.1"
  local ver_str=""
  ver_str="$(echo "$adf_basename" | sed -E 's/\.[aA][dD][fF]$//' | sed -E 's/^[Ww]orkbench//')"

  # Validate and preserve exact version
  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)"
}

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.
# This ensures a 3.2.x ADF build isn't hijacked when a 3.9 ISO is also selected.
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

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"

  if [[ ! -d "$WB35_DIR" ]]; then
    die "OS 3.9 CD missing expected directory: OS-Version3.9/Workbench3.5"
  fi

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

  # 2. Overlay WB 3.9 updates (overwrite base with newer versions)
  if [[ -d "$WB39_DIR" ]]; then
    echo "==> Overlaying Workbench 3.9 updated files"
    cp -R "$WB39_DIR"/* "$SYS_HOST/"
  fi

  # 3. Extract Boing Bag 1
  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

  # 4. Extract Boing Bag 2
  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

  # Flatten Boing Bag top-level directories into SYS_HOST
  for bb_dir in "$SYS_HOST"/BoingBag* "$SYS_HOST"/boingbag*; do
    if [[ -d "$bb_dir" ]]; then
      echo "  -> flattening $(basename "$bb_dir") into SYS_HOST"
      cp -R "$bb_dir"/* "$SYS_HOST/" 2>/dev/null || true
      rm -rf "$bb_dir"
    fi
  done

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

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

  # No first-boot flags needed for OS 3.9
  FIRSTBOOT_EXTRA=""
  OS32_MEDIA=0
  ensure_emu68_cmdline_files "$FAT_ROOT" "$FIRSTBOOT_EXTRA"

else
  # --- Standard ADF-based extraction (existing code) ---

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
    # Unknown locale naming variant, but still a locale disk.
    is_locale_adf=1
  fi

  if [[ "$is_locale_adf" -eq 1 ]]; then
    LOCALE_CANDIDATES+=("$a")
    # This is a locale ADF -- filter based on --locales selection
    if [[ -z "$LOCALES" ]]; then
      # No locale filter -- include all locale ADFs
      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 due explicit locale filter: $bn"
        LOCALE_SKIPPED=$((LOCALE_SKIPPED + 1))
      else
        echo "==> Skipping locale ADF (not selected): $bn"
        LOCALE_SKIPPED=$((LOCALE_SKIPPED + 1))
      fi
    fi
  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 locale filtering is active but no locale ADFs matched, fall back to include all locale candidates.
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 ADF."
  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 icon set selected but no 4ColorIcons ADF found; falling back to GlowIcons ADF."
  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"

# Windows parity: AmigaOS 3.2/3.2.x images need buptest/bupiter on first boot
# so SDH0 mounts reliably. Added ONLY to cmdline.txt (not cmdlineBAK.txt).
# Baseline had these and booted fine; CaffeineOS omits them but may not need them.
FIRSTBOOT_EXTRA=""
OS32_MEDIA=0
if echo "$(basename "$ISO_PATH")" | grep -qE '3\.2'; then
  FIRSTBOOT_EXTRA="buptest=512 bupiter=1"
  OS32_MEDIA=1
else
  for a in "${ADFS[@]}"; do
    if echo "$(basename "$a")" | grep -qiE '3\.2'; then
      FIRSTBOOT_EXTRA="buptest=512 bupiter=1"
      OS32_MEDIA=1
      break
    fi
  done
fi

if [[ -n "$FIRSTBOOT_EXTRA" ]]; then
  echo "==> FAT cmdline: enabling first-boot flags: $FIRSTBOOT_EXTRA"
else
  echo "==> FAT cmdline: no first-boot flags"
fi

# Rewrite cmdline files now that we know the OS version (safe overwrite)
ensure_emu68_cmdline_files "$FAT_ROOT" "$FIRSTBOOT_EXTRA"

# Locate Workbench ADF for Fix A (host-side)
for wb in "$ADF_SRC"/Workbench*.adf "$ADF_SRC"/Workbench*.ADF "$ADF_SRC"/Workbench3*.adf "$ADF_SRC"/Workbench3*.ADF; do
  [[ -f "$wb" ]] && { WORKBENCH_ADF="$wb"; break; }
done
detect_workbench_version

# Extract base ADFs in parallel, locale + icon ADFs sequentially after.
# Then patch/update ADFs sequentially last.
#
# Icon ADFs MUST extract after all other base ADFs so their files reliably
# overwrite earlier content. Locale ADFs share directories (Languages/,
# Catalogs/, etc.) and MUST be sequential to avoid race conditions — e.g.
# Locale3.2.3-DE.adf creates an empty Languages/ dir that clobbers the
# deutsch.language file from Locale-DE.adf during concurrent extraction.
#
# Parallel extraction has nondeterministic completion order, so both icon
# and locale ADFs are split out and run sequentially.
_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
  parallel_extract_adfs_to_host "$SYS_HOST" "${_base_parallel[@]}"
fi

# Locale ADFs: extract sequentially to avoid race conditions on shared directories
if [[ ${#LOCALE_ADFS[@]} -gt 0 ]]; then
  echo "==> Extracting ${#LOCALE_ADFS[@]} locale ADF(s) sequentially (shared directories)"
  for a in "${LOCALE_ADFS[@]}"; do
    extract_adf_to_host "$a" "$SYS_HOST"
  done
fi

# Icon ADFs: extract sequentially so they overlay base content deterministically
if [[ ${#SELECTED_ICONSET_ADFS[@]} -gt 0 ]]; then
  echo "==> Extracting ${#SELECTED_ICONSET_ADFS[@]} icon ADF(s) sequentially (must overlay base)"
  for a in "${SELECTED_ICONSET_ADFS[@]}"; do
    extract_adf_to_host "$a" "$SYS_HOST"
  done
fi
# Patch ADFs must run after all base + icon ADFs (they intentionally overwrite)
if [[ ${#PATCH_ADFS[@]} -gt 0 ]]; then
  echo "==> Extracting ${#PATCH_ADFS[@]} patch ADF(s) sequentially (order-dependent)"
  for a in "${PATCH_ADFS[@]}"; do
    extract_adf_to_host "$a" "$SYS_HOST"
  done
fi

fi  # end IS_39_ISO if/else

# Some OS 3.2 media put Locale-XX ADFs inside Install.adf instead of ISO/ADF root.
# Extract selected nested Locale ADFs so Locale prefs contains the requested languages.
if [[ -d "$SYS_HOST/Install" ]]; then
  echo "==> Checking nested locale ADFs in SYS:Install"
  _nested_locale_count=0
  for _nadf in "$SYS_HOST"/Install/Locale*.adf "$SYS_HOST"/Install/Locale*.ADF "$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
    else
      if [[ -n "$_ncode" ]]; then
        if echo ",$LOCALES," | grep -qi ",$_ncode,"; then
          _extract_nested=1
        fi
      fi
    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))
    elif [[ -n "$LOCALES" && -z "$_ncode" ]]; then
      echo "  -> skipping nested locale ADF with unknown language code (explicit locale filter active): $_nbn"
    fi
  done
  if [[ "$_nested_locale_count" -gt 0 ]]; then
    echo "==> Nested locale ADFs extracted: $_nested_locale_count"
  fi
fi

# Fallback: some ISOs provide Locale drawers directly at ISO root instead of Locale ADFs.
# Merge ISO Locale into the host payload so requested languages are available.
if [[ -d "$ISO_ROOT/Locale" ]]; then
  echo "==> Locale fallback: merging ISO root Locale drawer"
  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 extracted into nested folders (e.g. Locale-DE/Locale).
echo "==> Normalizing Locale drawers from extracted ADFs"
mkdir -p "$SYS_HOST/Locale" >/dev/null 2>&1 || true
while IFS= read -r -d '' _loc_dir; do
  _parent="$(dirname "$_loc_dir")"
  # Skip the canonical SYS_HOST/Locale itself
  if [[ "$_parent" == "$SYS_HOST" ]]; then
    continue
  fi
  cp -R "$_loc_dir/"* "$SYS_HOST/Locale/" 2>/dev/null || true
done < <(find "$SYS_HOST" -mindepth 2 -maxdepth 3 -type d -name Locale -print0 2>/dev/null)

# Locale ADFs place Catalogs/Help/Languages/Countries at the disk root.
# Merge those into SYS:Locale so Locale prefs can enumerate languages.
for _d in Catalogs Help Languages Countries; do
  if [[ -d "$SYS_HOST/$_d" ]]; then
    echo "==> Locale merge: $_d -> Locale/$_d"
    mkdir -p "$SYS_HOST/Locale/$_d" >/dev/null 2>&1 || true
    cp -R "$SYS_HOST/$_d/." "$SYS_HOST/Locale/$_d/" 2>/dev/null || true
  fi
done

# Windows parity: OS 3.2 update media ships .Z-compressed files (including locale catalogs).
# The Installer normally expands these; since we merge ADFs directly, do it here.
decompress_amiga_z_files() {
  local root="$1"
  command -v uncompress >/dev/null 2>&1 || {
    echo "==> WARN: 'uncompress' not available; leaving .Z files compressed."
    return 0
  }
  [[ -d "$root" ]] || return 0

  local count=0
  while IFS= read -r -d '' zfile; do
    if uncompress -f "$zfile" >/dev/null 2>&1; then
      count=$((count + 1))
    fi
  done < <(find "$root" -type f -iname "*.Z" -print0 2>/dev/null)

  if [[ "$count" -gt 0 ]]; then
    echo "==> Decompressed $count .Z files"
  fi
}

if [[ "$OS32_MEDIA" -eq 1 ]]; then
  echo "==> Decompressing .Z files from OS 3.2.x media (updates + locale catalogs)"
  decompress_amiga_z_files "$SYS_HOST"
fi

# Windows parity: keep WBStartup content from ADF merge, but move it under
# WBStartup/Disabled so only OnetimeRunWB runs during first Workbench boot.
if [[ -d "$SYS_HOST/WBStartup" ]]; then
  echo "==> Preparing host WBStartup: moving existing entries to WBStartup/Disabled"
  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 only the FIRSTTIMEBOOT flags from ADF Env-Archive (we manage those ourselves).
# Keep the rest of Env-Archive (screen mode prefs, locale, etc.) from the base OS.
for _ftb in FIRSTTIMEBOOT FIRSTTIMEBOOTWB REBOOT; do
  rm -f "$SYS_HOST/Prefs/Env-Archive/$_ftb" 2>/dev/null || true
done

# Ensure backdrop presets exist where Workbench expects them.
# Some media place default_*.iff at SYS: root during ADF extraction; if we only
# clean root junk later, Workbench can throw a requester for missing backdrop.
BACKDROP_PRESETS_DIR="$SYS_HOST/Prefs/Presets/Backdrops"
mkdir -p "$BACKDROP_PRESETS_DIR" >/dev/null 2>&1 || true

DEFAULT_PAL_TARGET="$BACKDROP_PRESETS_DIR/default_pal.iff"
if [[ ! -f "$DEFAULT_PAL_TARGET" ]]; then
  _pal_src=""
  for _cand in \
    "$SYS_HOST/default_pal.iff" \
    "$SYS_HOST/default.pal.iff" \
    "$SYS_HOST/default_ntsc.iff" \
    "$SYS_HOST/default.ntsc.iff" \
    "$SYS_HOST/Prefs/Presets/default_pal.iff" \
    "$SYS_HOST/Prefs/Presets/default.pal.iff" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/default_pal.iff" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/default.pal.iff" \
    ; do
    if [[ -f "$_cand" ]]; then
      _pal_src="$_cand"
      break
    fi
  done
  if [[ -n "$_pal_src" ]]; then
    cp -f "$_pal_src" "$DEFAULT_PAL_TARGET" >/dev/null 2>&1 || true
    if [[ -f "${_pal_src}.info" && ! -f "${DEFAULT_PAL_TARGET}.info" ]]; then
      cp -f "${_pal_src}.info" "${DEFAULT_PAL_TARGET}.info" >/dev/null 2>&1 || true
    fi
    echo "==> Backdrop preset restored: Prefs/Presets/Backdrops/default_pal.iff"
  fi
fi

DEFAULT_NTSC_TARGET="$BACKDROP_PRESETS_DIR/default_ntsc.iff"
if [[ ! -f "$DEFAULT_NTSC_TARGET" ]]; then
  _ntsc_src=""
  for _cand in \
    "$SYS_HOST/default_ntsc.iff" \
    "$SYS_HOST/default.ntsc.iff" \
    "$SYS_HOST/default_pal.iff" \
    "$SYS_HOST/default.pal.iff" \
    "$SYS_HOST/Prefs/Presets/default_ntsc.iff" \
    "$SYS_HOST/Prefs/Presets/default.ntsc.iff" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/default_ntsc.iff" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/default.ntsc.iff" \
    ; do
    if [[ -f "$_cand" ]]; then
      _ntsc_src="$_cand"
      break
    fi
  done
  if [[ -n "$_ntsc_src" ]]; then
    cp -f "$_ntsc_src" "$DEFAULT_NTSC_TARGET" >/dev/null 2>&1 || true
    if [[ -f "${_ntsc_src}.info" && ! -f "${DEFAULT_NTSC_TARGET}.info" ]]; then
      cp -f "${_ntsc_src}.info" "${DEFAULT_NTSC_TARGET}.info" >/dev/null 2>&1 || true
    fi
    echo "==> Backdrop preset restored: Prefs/Presets/Backdrops/default_ntsc.iff"
  fi
fi

PATTERN_TARGET="$BACKDROP_PRESETS_DIR/pattern.iff"
if [[ ! -f "$PATTERN_TARGET" ]]; then
  _pattern_src=""
  for _cand in \
    "$SYS_HOST/Prefs/Presets/Backdrops/pattern.iff" \
    "$SYS_HOST/pattern.iff" \
    "$DEFAULT_PAL_TARGET" \
    "$DEFAULT_NTSC_TARGET" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/pattern.iff" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/default_pal.iff" \
    "$ASSETS_DIR/AmigaFiles/System/Prefs/Presets/Backdrops/default_ntsc.iff" \
    ; do
    if [[ -f "$_cand" ]]; then
      _pattern_src="$_cand"
      break
    fi
  done
  if [[ -n "$_pattern_src" ]]; then
    cp -f "$_pattern_src" "$PATTERN_TARGET" >/dev/null 2>&1 || true
    if [[ -f "${_pattern_src}.info" && ! -f "${PATTERN_TARGET}.info" ]]; then
      cp -f "${_pattern_src}.info" "${PATTERN_TARGET}.info" >/dev/null 2>&1 || true
    fi
    echo "==> Backdrop preset restored: Prefs/Presets/Backdrops/pattern.iff"
  fi
fi

if [[ ! -f "$PATTERN_TARGET" && ! -f "$DEFAULT_PAL_TARGET" && ! -f "$DEFAULT_NTSC_TARGET" ]]; then
  echo "==> INFO: No custom backdrop image found (WBConfig backdrop mode still applies)"
fi

# --- Remove non-Workbench files from SYS_HOST root ---
# Our script extracts ALL ADFs (Workbench, Install, Extras, Storage, Fonts, Locale)
# into the same merge folder.  The Install/Extras ADFs contain files at their root
# (installer tools, background images, README, etc.) that should NOT appear on the
# final Workbench partition root.  The Windows reference tool avoids this by extracting
# specific files from each ADF; we clean up after the fact.
echo "==> Sanitizing SYS_HOST root: removing Install/Extras ADF junk"
_rm_count=0
# Some media place HDTools/HDToolBox at root. Preserve a usable SYS:Tools/HDToolBox.
if [[ -f "$SYS_HOST/HDToolBox" && ! -f "$SYS_HOST/Tools/HDToolBox" ]]; then
  mkdir -p "$SYS_HOST/Tools"
  cp -f "$SYS_HOST/HDToolBox" "$SYS_HOST/Tools/HDToolBox" 2>/dev/null || true
  [[ -f "$SYS_HOST/HDToolBox.info" ]] && cp -f "$SYS_HOST/HDToolBox.info" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
fi
if [[ -f "$SYS_HOST/HDTools" && ! -f "$SYS_HOST/Tools/HDToolBox" ]]; then
  mkdir -p "$SYS_HOST/Tools"
  cp -f "$SYS_HOST/HDTools" "$SYS_HOST/Tools/HDToolBox" 2>/dev/null || true
  [[ -f "$SYS_HOST/HDTools.info" ]] && cp -f "$SYS_HOST/HDTools.info" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
fi
if [[ ! -f "$SYS_HOST/Tools/HDToolBox" ]]; then
  # Fallback for OS 3.2 media: extract HDSetup*.adf and copy HDTools/HDToolBox.
  for _hdadf in "$SYS_HOST"/Install/HDSetup*.adf "$SYS_HOST"/Install/HDSetup*.ADF "$SYS_HOST"/Install/hdsetup*.adf "$SYS_HOST"/Install/hdsetup*.ADF; do
    [[ -f "$_hdadf" ]] || continue
    _hdtmp="$STAGE/hdsetup-adf-extract"
    rm -rf "$_hdtmp" >/dev/null 2>&1 || true
    mkdir -p "$_hdtmp"
    echo "==> Extracting HDSetup ADF for HDToolBox fallback: $(basename "$_hdadf")"
    extract_adf_to_host "$_hdadf" "$_hdtmp" || true
    _hd_candidate="$(find "$_hdtmp" -type f \( -iname 'HDToolBox' -o -iname 'HDTools' \) | head -n 1 || true)"
    if [[ -n "$_hd_candidate" ]]; then
      mkdir -p "$SYS_HOST/Tools"
      cp -f "$_hd_candidate" "$SYS_HOST/Tools/HDToolBox" 2>/dev/null || true
      # Find .info with case-insensitive search (ADF extraction may vary case)
      _hd_info="$(find "$(dirname "$_hd_candidate")" -maxdepth 1 -type f -iname "$(basename "$_hd_candidate").info" | head -n 1 || true)"
      if [[ -n "$_hd_info" ]]; then
        cp -f "$_hd_info" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
      fi
      break
    fi
  done
fi
if [[ ! -f "$SYS_HOST/Tools/HDToolBox" ]]; then
  _hd_candidate="$(find "$SYS_HOST" -type f \( -iname 'HDToolBox' -o -iname 'HDTools' \) | head -n 1 || true)"
  if [[ -n "$_hd_candidate" ]]; then
    echo "==> Found HDToolBox candidate outside SYS:Tools: $(basename "$_hd_candidate")"
    mkdir -p "$SYS_HOST/Tools"
    cp -f "$_hd_candidate" "$SYS_HOST/Tools/HDToolBox" 2>/dev/null || true
    _hd_info="$(find "$(dirname "$_hd_candidate")" -maxdepth 1 -type f -iname "$(basename "$_hd_candidate").info" | head -n 1 || true)"
    if [[ -n "$_hd_info" ]]; then
      cp -f "$_hd_info" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
    fi
  fi
fi
# Ensure HDToolBox.info exists even if binary was already in Tools/ from ADF extraction
if [[ -f "$SYS_HOST/Tools/HDToolBox" && ! -f "$SYS_HOST/Tools/HDToolBox.info" ]]; then
  echo "==> HDToolBox binary present but .info missing; searching for donor icon"
  _hd_info="$(find "$SYS_HOST" -type f -iname 'HDToolBox.info' -o -iname 'HDTools.info' 2>/dev/null | head -n 1 || true)"
  if [[ -n "$_hd_info" ]]; then
    cp -f "$_hd_info" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
    echo "==> Copied $(basename "$_hd_info") -> Tools/HDToolBox.info"
  else
    # Use a sibling tool icon as a generic fallback
    for _donor in "$SYS_HOST"/Tools/DiskDoctor.info "$SYS_HOST"/Tools/GraphicDump.info; do
      if [[ -f "$_donor" ]]; then
        cp -f "$_donor" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
        echo "==> Using $(basename "$_donor") as fallback icon for HDToolBox.info"
        break
      fi
    done
  fi
fi
# Installer tools
for _junk in \
  ActivateCDROM ActivateCDROM.info \
  HDSetup HDSetup.info HDTools HDTools.info \
  Installer Installer.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
# Background/pattern files (from Extras/Install ADFs)
for _bg in "$SYS_HOST"/background_*.png "$SYS_HOST"/Background_*.png; do
  [[ -f "$_bg" ]] && { rm -f "$_bg"; _rm_count=$((_rm_count+1)); }
done
# Different media use either dot or underscore naming.
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 _iff_info in default.ntsc.iff.info default.pal.iff.info default_ntsc.iff.info default_pal.iff.info pattern.iff.info; do
  [[ -f "$SYS_HOST/$_iff_info" ]] && { rm -f "$SYS_HOST/$_iff_info"; _rm_count=$((_rm_count+1)); }
done
# README / documentation dropped at root by Install ADF
for _doc in "$SYS_HOST"/Readme.txt "$SYS_HOST"/Readme.txt.info "$SYS_HOST"/ReadMe.txt "$SYS_HOST"/ReadMe.txt.info; do
  [[ -f "$_doc" ]] && { rm -f "$_doc"; _rm_count=$((_rm_count+1)); }
done
# Root-level command/program artifacts that belong inside System/ or Prefs/.
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
# LhA archive self-extraction leftovers (if present from manual runs or bad extract)
for _lha_junk in lha_68020 lha_68040 lha_68k lha_68k.readme LhA.guide; do
  [[ -f "$SYS_HOST/$_lha_junk" ]] && { rm -f "$SYS_HOST/$_lha_junk"; _rm_count=$((_rm_count+1)); }
done
# Directories from Install/Extras ADFs that are NOT standard Workbench drawers
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
# .info files for drawers that don't belong at root (Storage sub-drawers)
for _sub_info in DOSDrivers.info Monitors.info Keymaps.info Printers.info DataTypes.info; do
  [[ -f "$SYS_HOST/$_sub_info" ]] && { rm -f "$SYS_HOST/$_sub_info"; _rm_count=$((_rm_count+1)); }
done
echo "==> Removed $_rm_count non-Workbench items from SYS_HOST root"

# --- Reorganize fonts: move root-level .font files + data dirs into Fonts/ ---
# The Fonts ADF has font files at its root. After extraction they land at
# $SYS_HOST/ root level instead of $SYS_HOST/Fonts/. Detect and fix this.
_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
  echo "==> Reorganizing fonts: ${#_font_files[@]} .font file(s) found at SYS_HOST root"
  mkdir -p "$SYS_HOST/Fonts"
  for _ff in "${_font_files[@]}"; do
    _fname="$(basename "$_ff")"
    _stem="${_fname%.font}"
    # Move the .font descriptor
    mv "$_ff" "$SYS_HOST/Fonts/$_fname"
    # Move the matching font data directory (e.g. courier/ for courier.font)
    if [[ -d "$SYS_HOST/$_stem" ]]; then
      # Only move if it looks like a font data dir (contains numbered subdirs or files)
      mv "$SYS_HOST/$_stem" "$SYS_HOST/Fonts/$_stem"
    fi
  done
  # Also move .otag files (outline font tags) into Fonts/
  for _ot in "$SYS_HOST"/*.otag; do
    [[ -f "$_ot" ]] && mv "$_ot" "$SYS_HOST/Fonts/" 2>/dev/null || true
  done
  # Move _bullet* directories (outline font engine data) into Fonts/
  for _bd in "$SYS_HOST"/_bullet*; do
    [[ -d "$_bd" ]] && mv "$_bd" "$SYS_HOST/Fonts/" 2>/dev/null || true
  done
  echo "==> Fonts reorganized into SYS_HOST/Fonts/"
fi

# If SYS_HOST/Fonts exists but is a corrupt copy (contains non-font dirs like C/ or Devs/),
# clean it out and repopulate from the ISO or leave it with just the fonts we moved above.
if [[ -d "$SYS_HOST/Fonts/C" || -d "$SYS_HOST/Fonts/Devs" || -d "$SYS_HOST/Fonts/Libs" ]]; then
  echo "==> WARN: SYS_HOST/Fonts/ contains system directories (corrupt). Cleaning non-font entries."
  # Only keep font-related items inside Fonts/
  for _item in "$SYS_HOST/Fonts"/*; do
    _bname="$(basename "$_item")"
    case "$_bname" in
      *.font|*.otag|_bullet*) continue;;
    esac
    # Check if there's a matching .font file (font data directory)
    if [[ -f "$SYS_HOST/Fonts/${_bname}.font" ]]; then
      continue
    fi
    # Not a font-related item — remove it
    rm -rf "$_item" 2>/dev/null || true
  done
fi

# --- Reorganize keymaps: root-level Keymaps/ into Devs/Keymaps/ ---
# The Storage ADF ships keymaps at its root (Keymaps/d, Keymaps/gb, etc.).
# After extraction they land at $SYS_HOST/Keymaps/ instead of
# $SYS_HOST/Devs/Keymaps/ where AmigaOS expects them.
if [[ -d "$SYS_HOST/Keymaps" ]]; then
  _km_count=0
  mkdir -p "$SYS_HOST/Devs/Keymaps"
  for _km in "$SYS_HOST/Keymaps"/*; do
    [[ -e "$_km" ]] || continue
    _km_name="$(basename "$_km")"
    # Don't overwrite if OS already placed a keymap in Devs/Keymaps
    if [[ ! -e "$SYS_HOST/Devs/Keymaps/$_km_name" ]]; then
      cp -R "$_km" "$SYS_HOST/Devs/Keymaps/$_km_name"
      _km_count=$((_km_count + 1))
    fi
  done
  rm -rf "$SYS_HOST/Keymaps"
  echo "==> Keymaps reorganized: $_km_count keymap(s) merged into Devs/Keymaps/"
fi

 # 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 — no ADF collisions)"
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 in $ADF_SRC (skipping)"
fi

 # Fix A.1 (host-side): also restore a few core files from the ISO root drawers
# (ADF-only builds often miss these or produce drawer collisions)
fix_sys_host_file_collisions_from_iso "$ISO_ROOT" "$SYS_HOST"
# Final host-side canonicalization for core files (prevents file-vs-drawer collisions)
canonicalize_sys_host_core_paths "$SYS_HOST" "$ISO_ROOT"

#
# Copy merged SYS payload into the image
# Remove IconUpdate drawer (not needed for our images)
if [[ -d "$SYS_HOST/IconUpdate" || -f "$SYS_HOST/IconUpdate.info" ]]; then
  echo "==> Removing IconUpdate drawer from SYS payload"
  rm -rf "$SYS_HOST/IconUpdate" >/dev/null 2>&1 || true
  rm -f "$SYS_HOST/IconUpdate.info" >/dev/null 2>&1 || true
fi
# Safety: remove any stray IconUpdate drawers that may have landed in subpaths
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 (fast, host-side)
set_coloricons_borderless_host "$SYS_HOST"

# Sanitize non-ASCII characters in filenames/directories before copying to FFS image.
# hst-imager's FFS implementation crashes on characters like 'à' in 'català' when
# setting entry dates (PathNotFoundException). Transliterate to ASCII equivalents.
echo "==> Sanitizing non-ASCII filenames in SYS_HOST for FFS compatibility"
python3 -c "
import os, sys, unicodedata, shutil
host = sys.argv[1]
renamed = 0
removed = 0
# Walk depth-first so child renames don't break parent paths
for dirpath, dirnames, filenames in os.walk(host, topdown=False):
    for name in dirnames + filenames:
        ascii_name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode()
        if ascii_name != name and ascii_name:
            src = os.path.join(dirpath, name)
            dst = os.path.join(dirpath, ascii_name)
            if os.path.exists(dst):
                # ASCII version already exists — remove the non-ASCII duplicate
                if os.path.isdir(src):
                    shutil.rmtree(src)
                else:
                    os.remove(src)
                print(f'  -> removed duplicate: {name} (ASCII version {ascii_name} exists)')
                removed += 1
            else:
                os.rename(src, dst)
                print(f'  -> renamed: {name} -> {ascii_name}')
                renamed += 1
total = renamed + removed
if total:
    print(f'==> Sanitized {total} non-ASCII path(s) ({renamed} renamed, {removed} removed)')
" "$SYS_HOST" 2>/dev/null || echo "==> WARN: python3 not available for non-ASCII sanitization"

extract_host_to_sys "$SYS_HOST" "$SYS_ROOT"

# Ensure FIRSTTIMEBOOT flags exist (keeps OneTimeRunWB logic consistent)
ensure_firsttimeboot_flags_true "$SYS_ROOT"


# -----------------------------------------------------------------------------
# 6) Overlay additions from Assets WITHOUT clobbering the base AmigaOS install
# -----------------------------------------------------------------------------
# IMPORTANT:
# The Windows pipeline overlays only *additions* (startup scripts, PiStorm tools,
# optional programs, etc.) on top of a full AmigaOS install.
#
# Our Assets/AmigaFiles/System folder is NOT a complete Workbench installation
# (e.g. Assets/System/Tools only contains a couple of info files). If we copy
# these top-level drawers wholesale, we can wipe the real AmigaOS drawers and
# end up with missing commands (dir/list), missing SYS:Fonts, missing SYS:System/CLI,
# and Workbench errors like "Unable to open your tool 'Font'".
#
# Therefore we do a *non-destructive* overlay of only the known-safe additions.

echo "==> 6) 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 ! "$HST_IMAGER" fs dir "$_p" >/dev/null 2>&1; then
    echo "==> WARN: Expected base drawer missing after ADF merge: $_p" >&2
  fi
done

# --- 6a) 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
        ;;
    esac
    if [[ -d "$item" ]]; then
      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

# --- 6b) Overlay PiStorm drawer (additional tools/config; safe to add) ---
if [[ -d "$SYS_PAYLOAD/PiStorm" ]]; then
  echo "==> Overlay: System/PiStorm -> SYS:PiStorm"
  fs_copy_tree_filtered "$SYS_PAYLOAD/PiStorm" "$SYS_ROOT/PiStorm"
  apply_script_bits_for_tree "$SYS_PAYLOAD/PiStorm" "$SYS_ROOT/PiStorm"
fi

# --- 6c) 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

# --- 6d) Overlay optional drawers ---
# OneTimeRun and OneTimeRunWB are referenced as S:OneTimeRun and S:OneTimeRunWB
# in the startup scripts, so they must live under SYS:S/.
for _d in OneTimeRun OneTimeRunWB; do
  if [[ -d "$SYS_PAYLOAD/$_d" ]]; then
    echo "==> Overlay: System/$_d -> SYS:S/$_d"
    fs_copy_tree_filtered "$SYS_PAYLOAD/$_d" "$SYS_ROOT/S/$_d"
  fi
done
# Other top-level drawers go to SYS: root.
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

# --- 6d.1) Overlay ADDITIONAL files from Assets into core drawers (C, Devs, Libs) ---
# The Windows tool overlays Assets/System/C/, Devs/, Libs/ on top of the base AmigaOS
# install.  These contain extra commands (RemLib, SetPatch addons, etc.) and libraries
# that the overlay startup scripts rely on.
#
# We copy file-by-file so hst-imager adds files that don't already exist and
# overwrites those that do (Assets versions are expected to be correct).
# NOTE: Fonts, Storage, System, Tools, Utilities are NOT overlaid —
# the base AmigaOS source media is authoritative for those.
# Prefs IS overlaid because Assets provides Env-Archive/Sys prefs
# (screenmode, Workbench, Picasso96, ReqTools) needed by the Emu68 environment.

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")"
      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"

# --- 6e) Inject script overlays (Windows parity) ---
# Inject overlay fragments from Assets/System/S/ into Startup-Sequence and User-Startup.
# Startup-Sequence overlays add system detection, P96 switching, RexxMast, icon.library cleanup.
# User-Startup overlays add assigns for selected packages (Picasso96, Roadshow, AmiSSL, MUI38).
inject_startup_and_user_startup_overlays "$SYS_ROOT" "$SYS_PAYLOAD/S"

# --- 6f) Binary prefs patching (Windows parity) ---
write_screenmode_prefs "$SYS_ROOT" "$SCREEN_MODE_ID" "$WB_DEPTH_HEX"
write_wbconfig_prefs "$SYS_ROOT" "$BACKDROP_ENABLED"

# Set keyboard layout based on selected locale (first non-EN locale wins)
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

# --- 6g) Disk icons ---
install_disk_icons "$SYS_ROOT"
ensure_drawer_visible_in_root "$SYS_ROOT" "Programs"
ensure_drawer_visible_in_root "$SYS_ROOT" "PiStorm"
ensure_drawer_visible_in_root "$SYS_ROOT" "Emu68"

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

# Create Pi3/Pi4 HDToolbox icon variants with correct SCSI device tooltypes.
# Pi4vsPi3_Pistorm renames the matching variant to HDToolbox.info at first boot.
create_hdtoolbox_pi_variants "$SYS_ROOT"

# CLICon (Aminet: util/wb/CLICon) is needed by PiStorm OnlineWifi/OnlineGENet/Offline
# icons which use CLICon ToolTypes for script execution.
# On case-insensitive FFS, CLICon also satisfies Shell.info's DefaultTool=System/CLIcon.
# If download fails, fall back to copying CLI as CLIcon (basic shell, no script execution).
download_and_install_clicon "$SYS_ROOT"
if ! "$HST_IMAGER" fs dir "$SYS_ROOT/System/CLICon" >/dev/null 2>&1 \
   && ! "$HST_IMAGER" fs dir "$SYS_ROOT/System/CLIcon" >/dev/null 2>&1; then
  if "$HST_IMAGER" fs dir "$SYS_ROOT/System/CLI" >/dev/null 2>&1; then
    run "$HST_IMAGER" fs copy "$SYS_ROOT/System/CLI" "$SYS_ROOT/System/CLIcon" || true
    echo "==> Copied System/CLI -> System/CLIcon (WB3.2 LD fallback)"
  fi
fi

# mecho (Aminet: util/batch/mecho) is an enhanced echo command supporting escape
# sequences (\r, \n, etc.).  Required by S:ProgressBar which is called by the PiStorm
# Online script during network connection to show a progress animation.
download_and_install_mecho "$SYS_ROOT"

# WirelessManager (from prism2v2) is required by PiStorm Online script for WiFi.
# It associates with the WiFi access point before AddNetInterface can bring up wifipi.
if [[ "$NETWORK_STACK" == "roadshow" ]]; then
  download_and_install_wirelessmanager "$SYS_ROOT"
fi

# screennotify.library — required by WBDock2 for screen mode change notifications.
download_and_install_screennotify "$SYS_ROOT"

# icon.library 46.4 — enhanced icon library (installed via Aminet CSV when selected).
# Falls back to direct download only if user selected iconlib but CSV install missed it.
if package_is_selected "IconLib"; then
  if ! "$HST_IMAGER" fs dir "$SYS_ROOT/Libs/icon.library" >/dev/null 2>&1; then
    download_and_install_iconlib "$SYS_ROOT"
  fi
fi

# AHI 4.18 ships AHI_BGUI and AHI_MUI but no plain "AHI" or "AHI.MWB" binary.
# AHI.info (DefaultTool=AHI) and AHI.MWB.info (DefaultTool=AHI.MWB) both need a
# matching binary or clicking them shows "program not found".
# Use pull-to-host then push-back pattern (image→image copy is unreliable).
_ahi_prefs_src=""
if "$HST_IMAGER" fs dir "$SYS_ROOT/Libs/muimaster.library" >/dev/null 2>&1; then
  _ahi_prefs_src="AHI_MUI"
else
  _ahi_prefs_src="AHI_BGUI"
  # AHI_BGUI needs bgui.library — install from AHI archive dump if missing
  if ! "$HST_IMAGER" fs dir "$SYS_ROOT/Libs/bgui.library" >/dev/null 2>&1; then
    if "$HST_IMAGER" fs dir "$SYS_ROOT/Programs/AHI/BGUI/bgui.library" >/dev/null 2>&1; 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
# Pull the chosen AHI prefs binary to host, then push as both "AHI" and "AHI.MWB"
if "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/$_ahi_prefs_src" >/dev/null 2>&1; 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

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

# --- 6i) Set post-overlay icons borderless (covers Aminet packages, disk icons, etc.) ---
set_coloricons_borderless_image "$SYS_ROOT" "Tools" "System" "Prefs" "Programs" "PiStorm" "Emu68" "Utilities" "WBStartup" "Devs" "Storage"

# --- post-overlay sanity checks (surface issues early) ---
if ! "$HST_IMAGER" fs dir "$SYS_ROOT/System/CLI" >/dev/null 2>&1; then
  echo "==> WARN: SYS:System/CLI missing (Shell icon will fail)." >&2
fi
if ! "$HST_IMAGER" fs dir "$SYS_ROOT/Fonts" >/dev/null 2>&1; then
  echo "==> WARN: SYS:Fonts missing (Font prefs will fail)." >&2
fi
if ! "$HST_IMAGER" fs dir "$SYS_ROOT/Tools" >/dev/null 2>&1; 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\n" > "$TMP_ADF_NOTE"
else
  printf "adf-staged-from:%s\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.0.1) Helper: fetch mandatory LhA archive (Windows package model parity)
# -----------------------------------------------------------------------------
# Windows installs LhA through ListofPackagestoInstall CSV rows. On macOS we use
# the same CSV mapping and install at build-time; this helper only provides an
# archive source when --aminet-dir does not already contain one.
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
}

# -----------------------------------------------------------------------------
# 8) 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

# -----------------------------------------------------------------------------
# 8a) EMU68BOOT FAT partition support
# -----------------------------------------------------------------------------
# The Windows reference uses SD0Pi4/SD0Pi3 DOSDrivers (renamed to SD0 at first
# boot by Pi4vsPi3_Pistorm). EMU68BOOT: is an assign pointing to SD0: (set in
# amigaimager-assigns). No separate EMU68BOOT DOSDriver needed — it would
# conflict with SD0 (same brcm-emmc.device, same partition).
if [[ "$MOUNT_EMU68BOOT" == "1" ]]; then
  echo "==> EMU68BOOT: will be assigned to SD0: (fat95 + SD0 DOSDriver handles mount)"
  ensure_fat95_in_sys "$SYS_ROOT"
fi

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

TMP_STAGE_MARK="$STAGE/amigaimager-staged.txt"
printf "staged:%s\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 ! "$HST_IMAGER" fs dir "$SYS_ROOT/S" >/dev/null 2>&1; then
  die "Failed to create/access SYS:S at $SYS_ROOT/S"
fi
if ! "$HST_IMAGER" fs dir "$SYS_ROOT/S/OneTimeRunWB" >/dev/null 2>&1; then
  die "Failed to create/access SYS:S/OneTimeRunWB at $SYS_ROOT/S/OneTimeRunWB"
fi

# Keep the marker in SYS:S only (do NOT place it in S:OneTimeRunWB, since that drawer is executed).
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 expects ENVARC:Sys/Locale.prefs to exist. Ensure it is present by
# copying from Prefs/Presets if available.
if "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Env-Archive/Sys" >/dev/null 2>&1; then
  if "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Presets" >/dev/null 2>&1; then
    if "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Presets/Locale.prefs" >/dev/null 2>&1; then
      :
    fi
  fi
  # Copy (best-effort). hst-imager can copy within the same image.
  "$HST_IMAGER" fs copy "$SYS_ROOT/Prefs/Presets/Locale.prefs" "$SYS_ROOT/Prefs/Env-Archive/Sys/Locale.prefs" --force >/dev/null 2>&1 || true
  "$HST_IMAGER" fs copy "$SYS_ROOT/Prefs/Presets/Locale.prefs.info" "$SYS_ROOT/Prefs/Env-Archive/Sys/Locale.prefs.info" --force >/dev/null 2>&1 || true
fi

# Some startup scripts call the EMU68INFO command. If it is missing, Workbench shows
# "C:EMU68INFO: Unknown command". Provide a harmless stub so the boot flow stays clean.
TMP_EMU68INFO="$STAGE/EMU68INFO"
cat >"$TMP_EMU68INFO" <<'__EMU68INFO__'
; EMU68INFO stub (macOS build)
; Upstream environments may call C:EMU68INFO. This stub prevents "Unknown command".
QUIT 0
__EMU68INFO__
make_lf_only "$TMP_EMU68INFO"
write_uaem_with_perms "-s--rwed" "$TMP_EMU68INFO"
ensure_img_dir "$SYS_ROOT/C"
fs_copy_file "$TMP_EMU68INFO" "$SYS_ROOT/C/EMU68INFO" --uaemetadata UaeMetafile
fs_copy_file "$TMP_EMU68INFO" "$SYS_ROOT/C/emu68info" --uaemetadata UaeMetafile

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

if [[ "$DOWNLOAD_ONLY" == "1" ]]; then
  echo "==> Download-only mode: skipping Assets packages"
elif [[ -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"
    else
      echo "  -> WARN: package '$pkg' not found at $SRC (skipping)"
    fi
  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

# -----------------------------------------------------------------------------
# 6a.2) Stage Aminet packages - build-time installation (Windows approach)
# -----------------------------------------------------------------------------
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

  # Track packages installed via CSV mapping vs packages without mapping.
  declare -a AMINET_BUILDTIME=()
  declare -a AMINET_UNMAPPED=()
  declare -a AMINET_FILES=()
  LHA_READY=0

  # Build a deterministic package input list first.
  for lha_file in "$AMINET_DIR"/*.lha "$AMINET_DIR"/*.LHA; do
    [[ -f "$lha_file" ]] || 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

  # LhA is mandatory (Windows CSV rows). If not present in --aminet-dir,
  # attempt a quick Aminet fetch.
  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 (build-time install takes precedence).
    if [[ -n "${P96_ICOMP_ARCHIVE:-}" ]] && echo "$lha_base" | grep -qi "^picasso96\.lha$"; then
      echo "  -> skipping Aminet $lha_base (iComp P96 archive provided)"
      continue
    fi

    # Try build-time installation first (Windows approach)
    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

  # Windows parity on case-insensitive filesystems: treat C:lha / C:LhA as same command.
  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, or ensure Aminet download is reachable."
  fi
else
  echo "==> No Aminet packages to stage"
fi
normalize_roadie_install "$SYS_ROOT"

# --- Post-Aminet package fixups (Windows parity) ---

# AHI: The raw package dump in Programs/AHI/ has AHI.info (Tool icon) pointing
# to "AHI" which is a subdirectory (not a binary). Convert to Drawer so clicking
# opens the subdirectory instead of "program not found".
if hst_amiga_available && "$HST_IMAGER" fs dir "$SYS_ROOT/Programs/AHI/AHI" >/dev/null 2>&1; then
  ahi_tmp="$STAGE/ahi-icon-fix.$$"
  if fs_pull_file "$SYS_ROOT/Programs/AHI/AHI.info" "$ahi_tmp.info" >/dev/null 2>&1; then
    if "$HST_AMIGA" icon update "$ahi_tmp.info" -t 2 2>/dev/null; then
      fs_copy_file "$ahi_tmp.info" "$SYS_ROOT/Programs/AHI/AHI.info"
      echo "==> Converted Programs/AHI/AHI.info from Tool to Drawer"
    fi
    rm -f "$ahi_tmp.info" 2>/dev/null || true
  fi
fi

# DOpus416: Programs/DOpus.info is a Tool icon in the archive. Convert to Drawer
# so clicking it opens the drawer instead of trying to execute a directory.
if hst_amiga_available && "$HST_IMAGER" fs dir "$SYS_ROOT/Programs/DOpus" >/dev/null 2>&1; then
  dopus_tmp="$STAGE/dopus-drawer-fix.$$"
  if fs_pull_file "$SYS_ROOT/Programs/DOpus.info" "$dopus_tmp.info" >/dev/null 2>&1; then
    if "$HST_AMIGA" icon update "$dopus_tmp.info" -t 2 2>/dev/null; then
      fs_copy_file "$dopus_tmp.info" "$SYS_ROOT/Programs/DOpus.info"
      echo "==> Converted Programs/DOpus.info from Tool to Drawer"
    fi
    rm -f "$dopus_tmp.info" 2>/dev/null || true
  fi
fi

# HippoPlayer: archive has no drawer icon at Programs level. Create one from
# def_drawer.info so the folder is visible in Workbench (icon-view).
if "$HST_IMAGER" fs dir "$SYS_ROOT/Programs/HippoPlayer" >/dev/null 2>&1; then
  if ! "$HST_IMAGER" fs dir "$SYS_ROOT/Programs/HippoPlayer.info" >/dev/null 2>&1; then
    if "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info" >/dev/null 2>&1; then
      "$HST_IMAGER" fs copy "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info" "$SYS_ROOT/Programs/HippoPlayer.info" --force >/dev/null 2>&1 || true
      echo "==> HippoPlayer: created drawer icon from def_drawer.info"
    fi
  fi
fi

# DOpus416: enable FORCEOPENXAD ToolType (matches Windows reference)
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: download and 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: install at build time if folder provided
if [[ -n "${WHDLOAD_KICKSTARTS_DIR:-}" && -d "${WHDLOAD_KICKSTARTS_DIR:-}" ]]; then
  install_whdload_kickstarts "$SYS_ROOT" "$WHDLOAD_KICKSTARTS_DIR"
fi

# IMP3 registration code: write S:imp3.reg if provided and IMP3 was installed
if [[ -n "${IMP3_REG:-}" ]]; then
  if "$HST_IMAGER" fs dir "$SYS_ROOT/Programs/IMP3" >/dev/null 2>&1 \
     || "$HST_IMAGER" fs dir "$SYS_ROOT/C/IMP3" >/dev/null 2>&1; 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 in C:; skipping"
  fi
fi

# -----------------------------------------------------------------------------
# 6a.25) Extract Picasso96 archive (iComp or Aminet shareware fallback)
# -----------------------------------------------------------------------------
stage_p96_icomp_archive "$SYS_ROOT"
install_emu68_tools "$SYS_ROOT"
install_genet_driver "$SYS_ROOT"
ensure_p96_videocore_driver "$SYS_ROOT"

# Ensure Emu68 folder is visible with drawer icon (if created by install_emu68_tools)
if "$HST_IMAGER" fs dir "$SYS_ROOT/Emu68" >/dev/null 2>&1; then
  if ! "$HST_IMAGER" fs dir "$SYS_ROOT/Emu68.info" >/dev/null 2>&1; then
    if "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info" >/dev/null 2>&1; then
      "$HST_IMAGER" fs copy "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info" "$SYS_ROOT/Emu68.info" --force >/dev/null 2>&1 || true
    elif "$HST_IMAGER" fs dir "$SYS_ROOT/Tools.info" >/dev/null 2>&1; then
      "$HST_IMAGER" fs copy "$SYS_ROOT/Tools.info" "$SYS_ROOT/Emu68.info" --force >/dev/null 2>&1 || true
    fi
    echo "==> Emu68: folder made visible with drawer icon"
  fi
fi

# -----------------------------------------------------------------------------
# 6a.3) Extract Roadshow bsdsocket.library if needed
# -----------------------------------------------------------------------------
extract_roadshow_bsdsocket "$SYS_ROOT"

# -----------------------------------------------------------------------------
# 6a.4) Configure Amiga-side networking (Roadshow + WiFi)
# -----------------------------------------------------------------------------
configure_amiga_network "$SYS_ROOT"

# -----------------------------------------------------------------------------
# Finalize RunAll LAST
# -----------------------------------------------------------------------------
echo "==> Finalize RunAll (must be a file)"

TMP_RUNALL="$STAGE/RunAll"
{
  echo "; Amiga Imager - RunAll optional installers"
  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
      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

  # NOTE: Picasso96 iComp is now extracted at build time (stage_p96_icomp_archive)
  # so no first-boot installer entry is needed here.

  # UHC Tools: run online installer on first boot (requires Roadshow + internet).
  # The Installation script downloads tools from UHC mirrors via aget.
  if package_is_selected "UHCTools"; then
    echo ""
    echo "; UHC Tools — online installer (requires network)"
    echo "IF EXISTS \"SYS:Programs/UHCTools/Installation\""
    echo "  ECHO \"-> UHC Tools: running online installer...\""
    echo "  CD SYS:Programs/UHCTools"
    echo "  Execute Installation"
    echo "  CD SYS:"
    echo "ENDIF"
  fi

  # Backdrop: now handled at build time by write_wbconfig_prefs() (patches byte 51).
  # No first-boot ARexx workaround needed — PC reference parity.

  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"
ensure_img_dir "$SYS_ROOT/S/OneTimeRunWB"
fs_copy_file "$TMP_RUNALL" "$SYS_ROOT/S/OneTimeRunWB/RunAll"

# Backdrop: handled at build time by write_wbconfig_prefs() (byte 51 patch).
# Old ARexx WBStartup/EnableBackdrop workaround removed — PC reference parity.

# -----------------------------------------------------------------------------
# 6b) 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"

# Prefer upstream OnetimeRunWB script from Assets (Windows parity).
# Fall back to an embedded equivalent if missing.
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"
   IF $System EQ "UAE"
      LIST >>T:RunTimeScriptWB S:OneTimeRunWB PAT=~(#?.info|#?_pistorm) LFORMAT="ECHO *"Running Script: %n*"*nEcho *"*"*nExecute *"%p%n*"*nDelete *"%p%n*"*n" ALL FILES
   ELSE
      LIST >>T:RunTimeScriptWB S:OneTimeRunWB PAT=~(#?.info|) LFORMAT="ECHO *"Running Script: %n*"*nEcho *"*"*nExecute *"%p%n*"*nDelete *"%p%n*"*n" ALL FILES
   ENDIF
   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 SDH0:
   IF $System EQ "UAE"
      Sys:c/reboot
   ELSE
      c:EMU68INFO HARDRESET
   ENDIF
   ENDCLI
ENDIF
__WB_OTR__
fi
make_lf_only "$TMP_WB_OTR"
fs_copy_file_no_makedir "$TMP_WB_OTR" "$SYS_ROOT/WBStartup/OnetimeRunWB"

# Install .info if present in Assets (nice-to-have only)
if [[ -f "$ASSET_WB_OTR_INFO" ]]; then
  fs_copy_file "$ASSET_WB_OTR_INFO" "$SYS_ROOT/WBStartup/OnetimeRunWB.info"
fi

# Windows parity: keep other WBStartup items disabled until first Workbench boot.
ensure_img_dir "$SYS_ROOT/WBStartup/Disabled"

# Install additional WBStartup files from Assets (Menutools, etc.) into Disabled/
# and skip OnetimeRunWB / OnetimeRunWB.info (handled above).
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 (Assets may ship only .info files).
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 so missing optional payloads do not produce requesters/errors.
harden_first_boot_scripts "$SYS_ROOT"

# -----------------------------------------------------------------------------
# Install IBrowse/AmiSSL from official website (AFTER hardening to override Ibrowse script)
# -----------------------------------------------------------------------------
if [[ -n "$IBROWSE_DIR" && -d "$IBROWSE_DIR" ]]; then
  install_ibrowse_from_official "$SYS_ROOT" "$IBROWSE_DIR" "${IBROWSE_KEY:-}"
fi
# Also handle key-only workflows where IBrowse itself comes from media/packages.
install_ibrowse_key_file "$SYS_ROOT" "${IBROWSE_KEY:-}" "${IBROWSE_DIR:-}"

# -----------------------------------------------------------------------------
# Install WBDock2 dock launcher (AFTER IBrowse so dock config paths are valid)
# -----------------------------------------------------------------------------
if [[ "$INCLUDE_WBDOCK" -eq 1 && -n "$IBROWSE_DIR" && -d "$IBROWSE_DIR" ]]; then
  install_wbdock "$SYS_ROOT" "$IBROWSE_DIR"
fi

# -----------------------------------------------------------------------------
# NewMeter — position window in upper-right corner
# -----------------------------------------------------------------------------
configure_newmeter_position "$SYS_ROOT"

# NOTE: hst-imager has no fs rm/delete, so WBStartup items must be installed
# to WBStartup/Disabled/ from the start (CSV or install functions).
# OnetimeRunWB copies them back before rebooting; they run on 2nd boot.

# -----------------------------------------------------------------------------
# Windows parity helpers (kept for troubleshooting, disabled by default)
# -----------------------------------------------------------------------------
# The Windows reference image applies ScreenMode directly from startup scripts
# and uses foreground IPrefs. Keep that behavior as default.
# defer_startup_screenmode_use_to_iprefs() — REMOVED: Windows parity uses direct ScreenMode USE calls.
# run_iprefs_detached() — REMOVED: superseded by IPrefs removal from Startup-Sequence.

# -----------------------------------------------------------------------------
# Remove IPrefs from Startup-Sequence — WBStartup IPrefs handles the switch
# -----------------------------------------------------------------------------
# Root cause of "Intuition is attempting to reset the Workbench screen":
#   1. CheckScreenModeandChipset_Pistorm (OneTimeRun) renames screenmode.prefs.user
#      (RTG) over screenmode.prefs — so prefs now say RTG.
#   2. Monitors load → VideoCore/UAEGFX RTG modes registered with Picasso96.
#   3. IPrefs (Startup-Sequence, BEFORE LoadWB) reads RTG prefs and asks Intuition
#      to switch the WB screen from native to RTG.  The AmigaDOS startup shell
#      window is STILL OPEN on the WB screen → Intuition fires the requester.
#
# Fix: remove IPrefs from the Startup-Sequence entirely.
# The standard WB3.2 SYS:WBStartup/IPrefs runs AFTER LoadWB + EndCLI.
# At that point Workbench *cooperates* with the screen-mode reset (it closes its
# own windows, lets Intuition reopen the screen in RTG, then re-opens them) —
# no manual "please close AmigaDOS" requester is ever shown on any boot.
# relocate_iprefs_after_loadwb()
#   Move IPrefs from its original position (between AddDataTypes and ConClip,
#   BEFORE LoadWB) to AFTER LoadWB as a background process.
#
#   Why: IPrefs in its original position reads RTG screenmode.prefs while the
#   AmigaDOS CLI window is still open → Intuition "please close AmigaDOS"
#   requester.
#
#   The fix is: relocate IPrefs AFTER LoadWB via `Run >NIL: IPrefs`.
#   LoadWB opens with native mode initially, then IPrefs reads RTG prefs
#   and switches the screen. EndCLI closes the shell before IPrefs processes
#   the switch, so no blocking windows → no requester. Confirmed by
#   reference image dump that boots in RTG with this exact IPrefs position.
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

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

  # Find the IPrefs invocation line (plain or 'Run >NIL: IPrefs' form)
  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

  # Find the EndCLI line (insert IPrefs just before it)
  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}"

  # Check if a delayed WBDock launcher script exists (created by install_wbdock)
  local has_wbdock_launcher=0
  if "$HST_IMAGER" fs dir "$sys_root/S/StartWBDock" >/dev/null 2>&1; then
    has_wbdock_launcher=1
  fi

  awk -v iprefs_ln="$iprefs_line" -v endcli_ln="$endcli_line" -v wbdock="$has_wbdock_launcher" '
    NR == iprefs_ln {
      # Remove IPrefs from its original position
      print "; Amiga Imager: IPrefs relocated after LoadWB (avoid RTG requester)"
      next
    }
    NR == endcli_ln {
      # Insert background IPrefs right before EndCLI.
      # IPrefs switches the WB screen from native to RTG. EndCLI closes
      # the startup shell before IPrefs processes the switch → no blocking
      # window → no Intuition requester.
      # All non-OnetimeRunWB WBStartup items are in WBStartup/Disabled/,
      # and OnetimeRunWB opens its console with AUTO (not until ECHO output
      # after WAIT 1), so IPrefs has a clean window to switch.
      print "Run >NIL: IPrefs"
      if (wbdock == 1) {
        print "; Amiga Imager: delayed WBDock launch (after IPrefs switches to RTG)"
        print "Run >NIL: Execute SYS:S/StartWBDock"
      }
      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"
    fs_copy_file "$tmp_ss" "$sys_root/S/Startup-Sequence"
    echo "==> Patched: IPrefs relocated after LoadWB (Run >NIL: IPrefs before EndCLI)"
    if [[ "$has_wbdock_launcher" -eq 1 ]]; then
      echo "==> Patched: WBDock delayed launch added to Startup-Sequence"
    fi
  else
    rm -f "$tmp_ss.new"
    echo "==> IPrefs relocation: no change needed"
  fi
}
# Relocate IPrefs after LoadWB: IPrefs runs via `Run >NIL: IPrefs` after
# LoadWB but before EndCLI. EndCLI closes the shell before IPrefs processes
# the mode switch, so there is no blocking window and no Intuition requester.
# All non-essential WBStartup items are in WBStartup/Disabled/ (copied back
# by OnetimeRunWB before reboot), and OnetimeRunWB itself uses AUTO console
# (no window until after WAIT 1), so IPrefs has a clean switch window.
# If WBDock is installed, a delayed launcher is also added after IPrefs.
relocate_iprefs_after_loadwb "$SYS_ROOT"

# -----------------------------------------------------------------------------
# Sanity check
# -----------------------------------------------------------------------------
echo "==> Sanity check (key dirs on SYS: and FAT:)"
run "$HST_IMAGER" fs dir "$FAT_ROOT" || true
run "$HST_IMAGER" fs dir "$SYS_ROOT" || true
run "$HST_IMAGER" fs dir "$SYS_ROOT/S" --recursive || true
run "$HST_IMAGER" fs dir "$SYS_ROOT/WBStartup" --recursive || true
run "$HST_IMAGER" fs dir "$SYS_ROOT/S/OneTimeRunWB" --recursive || true
run "$HST_IMAGER" rdb info "$RDB" --verbose || true

# Sanity: detect file-vs-directory using fs_pull_file (fs dir succeeds for both).
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 "$HST_IMAGER" fs dir "$SYS_ROOT/System/CLI" >/dev/null 2>&1; 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 "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Font" >/dev/null 2>&1; then
  echo "==> WARN: Prefs/Font is a directory (should be a file)"
elif "$HST_IMAGER" fs dir "$SYS_ROOT/Prefs/Font.Z" >/dev/null 2>&1; 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

# System/Workbench check removed — WB3.2 uses C:LoadWB, not System/Workbench.

echo "==> DONE: $IMG"
