#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# build-pistorm.sh — PiStorm/Emu68 platform build script
#
# Creates bootable SD card images for PiStorm accelerator running Emu68.
# Image layout: MBR with FAT32 boot partition + RDB Amiga partitions.
#
# Sources shared functions from lib/common.sh and partition_layout.sh.
# ---------------------------------------------------------------------------

set -euo pipefail

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

SCRIPT_VERSION="${AMIGA_IMAGER_APP_VERSION:-dev}"
echo "==> Amiga-Imager 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=""
PARTITION_CONTENT_MAP=""
ADF_DIR=""              # Optional: merged ADF folder
EMU68_BOOT_URL=""       # Optional: override Emu68 boot zip URL
EMU68_BOOT_MODE="auto"  # auto | assets | download
EMU68_PRERELEASE=0      # 1 = allow downloading latest prerelease/beta Emu68 firmware
ICON_SET="standard"     # standard | glowicons
BUILD_MANIFEST=""       # optional: path to JSON manifest written by Swift (Stage D)
BUILD_PLAN=""           # optional: path to compiled shell execution plan (Phase 2A/2B)
PACKAGES=""             # comma-separated package ids resolved by Swift
LOCALES=""              # comma-separated locale codes (en,de,fr) or empty = all
CSV_DIR=""              # explicit InputFiles directory (overrides find_input_csv search)
BLANK_AMIGA_PARTITIONS=0 # 1 = keep FAT boot setup, leave Amiga partitions empty
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__"
P96_ICOMP_ARCHIVE=""    # optional local Picasso96 iComp archive (.lha)
PISTORM_MODERN_P96_MODE="0" # legacy explicit override; modern PiStorm mode now auto-enables when archive is supplied
IBROWSE_DIR=""          # optional staging dir for IBrowse installer (.lha)
IBROWSE_KEY=""          # optional IBrowse key file
GOVD_KEY=""             # optional GoVD registration key file
INCLUDE_IBROWSE=0       # 1=install IBrowse from staging dir
INCLUDE_WBDOCK=0        # 1=install WBDock2 from staging dir
INCLUDE_BASS=0          # 1=download and install Beneath a Steel Sky freeware
INCLUDE_FOTAQ=0         # 1=download and install Flight of the Amazon Queen freeware
INCLUDE_LURE=0          # 1=download and install Lure of the Temptress freeware
INCLUDE_GOVD=0          # 1=install GoVD virtual desktop
WHDLOAD_KICKSTARTS_DIR="" # optional folder with Kickstart ROM files for WHDLoad
IMP3_REG=""              # optional IMP3 registration code (written to S:imp3.reg)
IMP3_REG_FILE=""         # optional path to imp3.reg file (takes precedence over 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
APPLY_ICON_POSITIONS="1" # 1|0 apply icon positions from CSV at build time
ICON_PASS2_MODE="scoped" # scoped|global for the late optional-package icon pass
DETECTED_WB_VERSION="3.2"  # auto-detected from ADF filename
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_BLOCKSIZES=()  # bytes per filesystem block
PART_BOOTABLE=()    # "1" or "0"
PART_BOOTPRI=()     # integer

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

# Unicam (Phase 2)
UNICAM_ENABLE=""
UNICAM_MODE="smooth"
UNICAM_TYPE=""
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"
HST_IMAGER="${HST_IMAGER:-$DEFAULT_HST}"
AMIGA_TOOLS="${AMIGA_TOOLS:-}"


# ===========================================================================
# PiStorm-specific functions
# ===========================================================================
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 the Emu68 release tag to use, writing it to a temp file so it
# survives the subshell boundary when the caller uses $(...).
# EMU68_PRERELEASE=0 → /releases/latest tag (stable only).
# EMU68_PRERELEASE=1 → /releases list, picks the first tag with "prerelease":true.
# No python3 required; uses awk + sed only.
EMU68_RELEASE_TAG=""
_resolve_emu68_release_tag() {
  local tag=""

  if [[ "${EMU68_PRERELEASE:-0}" -eq 1 ]]; then
    echo "==> Emu68: querying GitHub for latest prerelease" >&2
    local releases_json
    releases_json="$(curl -fsSL "https://api.github.com/repos/michalsc/Emu68/releases" 2>/dev/null \
      | tr -d '\r' || true)"
    if [[ -n "$releases_json" ]]; then
      # GitHub returns pretty-printed JSON; tag_name appears before prerelease within each object.
      # Strategy: track the most recently seen tag_name; when we hit "prerelease":true, emit it.
      tag="$(printf "%s" "$releases_json" | awk '
        /"tag_name"/ {
          line = $0
          sub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, "", line)
          sub(/".*/, "", line)
          if (line != "") last = line
        }
        /"prerelease"[[:space:]]*:[[:space:]]*true/ {
          if (last != "") { print last; exit }
        }
      ' 2>/dev/null || true)"
    fi
    if [[ -n "$tag" ]]; then
      echo "==> Emu68: prerelease selected: $tag" >&2
    else
      echo "==> WARN: No Emu68 prerelease found; falling back to latest stable" >&2
    fi
  fi

  if [[ -z "$tag" ]]; then
    local stable_json
    stable_json="$(curl -fsSL "https://api.github.com/repos/michalsc/Emu68/releases/latest" 2>/dev/null \
      | tr -d '\r' || true)"
    tag="$(printf "%s" "$stable_json" \
      | sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' \
      | head -n1 || true)"
    [[ -n "$tag" ]] && echo "==> Emu68: stable selected: $tag" >&2
  fi

  printf "%s" "$tag"
}

# Return URLs for all PiStorm variant zips, one per line.
# URLs are constructed from the resolved tag name + known filename patterns —
# no JSON asset-list parsing needed, so no python3 and no greedy-sed issues.
# v1.0.x: Emu68-pistorm32lite.zip + Emu68-pistorm.zip (GPIO-based config.txt selector)
# v1.1.x: Emu68-pistorm-classic.zip + Emu68-pistorm.zip (unified binary, no GPIO)
# Also writes the tag to $STAGE/emu68-release-tag.txt so the caller can recover
# EMU68_RELEASE_TAG after the subshell boundary.
resolve_emu68_pistorm_all_zip_urls() {
  if [[ -n "$EMU68_BOOT_URL" ]]; then
    printf "%s\n" "$EMU68_BOOT_URL"
    return 0
  fi

  local tag
  tag="$(_resolve_emu68_release_tag)"
  if [[ -z "$tag" ]]; then
    echo "==> ERROR: Could not resolve Emu68 release tag from GitHub API" >&2
    return 1
  fi

  # Persist tag for the parent shell (lost across subshell boundary)
  if [[ -n "${STAGE:-}" ]]; then
    printf "%s" "$tag" > "$STAGE/emu68-release-tag.txt" 2>/dev/null || true
  fi

  local base="https://github.com/michalsc/Emu68/releases/download/${tag}"

  # Detect version format from tag string:
  # v1.1.x and higher → new unified format (pistorm-classic + pistorm)
  # v1.0.x and below  → legacy multi-variant format (pistorm32lite + pistorm)
  # v1.1+ (minor >= 1) → new unified format; v1.0.x → legacy multi-variant format
  if printf "%s" "$tag" | grep -qE '^v[0-9]+\.([1-9][0-9]*)'; then
    printf "%s/Emu68-pistorm-classic.zip\n" "$base"
    printf "%s/Emu68-pistorm.zip\n" "$base"
  else
    printf "%s/Emu68-pistorm32lite.zip\n" "$base"
    printf "%s/Emu68-pistorm.zip\n" "$base"
  fi
}

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
    # pistorm16 uses a 16-bit bus (A600), same as pistorm (A500). Prefer pistorm over
    # pistorm32lite (A1200 32-bit bus) — different GPIO layout, guaranteed wrong hardware.
    local k16_src="${kps:-${k32:-$fallback}}"
    echo "==> Boot payload: missing Emu68-pistorm16, creating alias from $(basename "$k16_src")"
    cp -f "$k16_src" "$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
}

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
    "$AMIGA_TOOLS" regex-replace --case-insensitive "$tmp_cfg" '^[ \t]*initramfs[ \t]+.*' 'initramfs kick.rom'
  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"

  # v1.0.x + FrameThrower: firmware reads these directly from cmdline.txt.
  # v1.1.x uses dtoverlay in config.txt instead (handled in write_phase2_config).
  if [[ "${UNICAM_ENABLE:-}" == "1" && "${EMU68_BUNDLE_FORMAT:-v1}" != "v2" ]]; then
    local _unicam_mode="${UNICAM_MODE:-smooth}"
    if [[ "${UNICAM_AUTOSTART:-1}" != "0" ]]; then
      base="$base unicam.boot unicam.$_unicam_mode"
    else
      base="$base unicam.$_unicam_mode"
    fi
  fi

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

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

  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
  # Enforce TOOL type (do_Type=3) — GlowIcons base may ship with wrong type
  hst_icon_update_in_place "$pi3_info" "3" 2>/dev/null || true
  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
  # Enforce TOOL type (do_Type=3) — GlowIcons base may ship with wrong type
  hst_icon_update_in_place "$pi4_info" "3" 2>/dev/null || true
  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)

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"
      chmod +x "$tmp_ss"  # Amiga 's' bit: script protection for FFS
      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"
  chmod +x "$tmp_wb"  # Amiga 's' bit: required for WBStartup script execution
  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"
    chmod +x "$tmp_cmd"  # Amiga 's' bit: required for Execute in OnetimeRunWB
    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 specific codec files exist (dispatch-aware — works with both engines)
    local has_codecs=0
    if fs_dir "$sys_root/Programs/Ibrowse/codecs/gif.codec" ||        fs_dir "$sys_root/Programs/Ibrowse/codecs/jpeg.codec" ||        fs_dir "$sys_root/Programs/Ibrowse/codecs/png.codec"; then
      has_codecs=1
    fi
    echo "==> Hardening: IBrowse codecs present = $has_codecs"
    if [[ "$has_codecs" -eq 1 ]]; then
      # 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"
    chmod +x "$tmp_ib"  # Amiga 's' bit: required for Execute in T:RunTimeScript
    fs_copy_file "$tmp_ib" "$sys_root/S/OneTimeRun/Ibrowse"
  fi
}

download_and_install_wirelessmanager() {
  local sys_root="$1"

  # Skip if wirelessmanager already exists on image
  if fs_dir "$sys_root/C/wirelessmanager" || \
     fs_dir "$sys_root/C/WirelessManager"; 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


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


# ===========================================================================
# Emu68 tools, genet driver, Pi-specific config
# ===========================================================================
# 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_emu68_release_videocore_card()
#   For Emu68 v1.1.x bundles: download VideoCore.card directly from the Emu68
#   release assets (the card driver there is newer than the one in Emu68-tools).
#   Overwrites whatever install_emu68_tools put in LIBS:Picasso96/.
#   $1 = sys_root
install_emu68_release_videocore_card() {
  local sys_root="$1"
  [[ "${EMU68_BUNDLE_FORMAT:-v1}" == "v2" ]] || return 0

  # Recover tag from temp file if the global was lost in a subshell
  if [[ -z "${EMU68_RELEASE_TAG:-}" && -f "$STAGE/emu68-release-tag.txt" ]]; then
    EMU68_RELEASE_TAG="$(cat "$STAGE/emu68-release-tag.txt" 2>/dev/null || true)"
  fi
  [[ -n "${EMU68_RELEASE_TAG:-}" ]] || { echo "==> WARN: EMU68_RELEASE_TAG not set; skipping release VideoCore.card download"; return 0; }

  local card_url="https://github.com/michalsc/Emu68/releases/download/${EMU68_RELEASE_TAG}/VideoCore.card"
  local tmp_card="$STAGE/VideoCore-release.card.$$.$RANDOM"
  echo "==> Emu68 v1.1.x: downloading VideoCore.card from release ${EMU68_RELEASE_TAG}"

  if ! curl -fL --retry 3 --retry-delay 2 -o "$tmp_card" "$card_url"; then
    echo "==> WARN: Failed to download VideoCore.card from $card_url"
    return 0
  fi

  local size
  size="$(stat -f%z "$tmp_card" 2>/dev/null || stat -c%s "$tmp_card" 2>/dev/null || echo 0)"
  if [[ "$size" -lt 1000 ]]; then
    echo "==> WARN: Downloaded VideoCore.card is suspiciously small ($size bytes); skipping"
    rm -f "$tmp_card"
    return 0
  fi

  echo "==> Installing VideoCore.card ${EMU68_RELEASE_TAG} ($size bytes) -> LIBS:Picasso96/"
  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
  rm -f "$tmp_card" 2>/dev/null || true
}

detect_p96_rtg_library_major() {
  local root="$1"
  local rtg_path=""
  local rtg_ver="0"

  for rtg_path in \
    "$root/Libs/Picasso96/rtg.library" \
    "$root/libs/Picasso96/rtg.library" \
    "$root/libs/picasso96/rtg.library"
  do
    if [[ -f "$rtg_path" ]]; then
      rtg_ver="$(strings "$rtg_path" 2>/dev/null \
        | sed -n 's/.*rtg\.library \([0-9][0-9]*\)\..*/\1/p' | head -n1)" || true
      break
    fi
  done

  echo "${rtg_ver:-0}"
}

detect_installed_videocore_version_code() {
  local sys_root="$1"
  local tmp_card="$STAGE/videocore-version.$$.$RANDOM"
  local version_line="" major="0" minor="0"

  if ! fs_pull_file "$sys_root/Libs/Picasso96/VideoCore.card" "$tmp_card" >/dev/null 2>&1; then
    rm -f "$tmp_card" 2>/dev/null || true
    echo "0"
    return 0
  fi

  version_line="$(strings "$tmp_card" 2>/dev/null \
    | sed -n 's/.*VideoCore \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2/p' | head -n1)" || true
  rm -f "$tmp_card" 2>/dev/null || true

  if [[ -n "$version_line" ]]; then
    read -r major minor <<< "$version_line"
  fi

  major="${major:-0}"
  minor="${minor:-0}"
  echo $(( major * 1000 + minor ))
}

reapply_vc4_legacy_id_tooltype() {
  local sys_root="$1"
  local target="" tmp_info="" tmp_tt=""

  tmp_info="$STAGE/vc4-legacy-info.$$.$RANDOM"
  tmp_tt="$STAGE/vc4-legacy-tooltype.$$.$RANDOM"
  printf 'VC4_LEGACY_ID\n' > "$tmp_tt"

  for target in \
    "$sys_root/Devs/Monitors/Videocore.info" \
    "$sys_root/Storage/Monitors/Uaegfx.info"
  do
    if ! fs_pull_file "$target" "$tmp_info" >/dev/null 2>&1; then
      continue
    fi
    if apply_tooltype_overrides_in_place "$tmp_info" "modify" "$tmp_tt"; then
      fs_copy_file "$tmp_info" "$target"
    fi
    rm -f "$tmp_info" 2>/dev/null || true
  done

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

pistorm_modern_p96_experiment_enabled() {
  if [[ -n "${P96_ICOMP_ARCHIVE:-}" ]]; then
    return 0
  fi
  case "${PISTORM_MODERN_P96_MODE:-0}" in
    1|true|TRUE|yes|YES|on|ON) return 0 ;;
  esac
  case "${PISTORM_MODERN_P96_EXPERIMENT:-0}" in
    1|true|TRUE|yes|YES|on|ON) return 0 ;;
    *) return 1 ;;
  esac
}

restage_modern_p96_core_for_videocore() {
  local sys_root="$1"
  local p96_root=""
  local user_rtg_ver="0"
  local vc_version_code="0"
  local saved_trust="${P96_TRUST_ARCHIVE_ABI-__unset__}"

  p96_root="$(find "$STAGE/p96-extract" -maxdepth 1 -type d -name "Picasso96*" | head -n1)"
  [[ -d "$p96_root" ]] || return 0

  user_rtg_ver="$(detect_p96_rtg_library_major "$p96_root")"
  [[ "${user_rtg_ver:-0}" -ge 41 ]] || return 0

  vc_version_code="$(detect_installed_videocore_version_code "$sys_root")"
  [[ "${vc_version_code:-0}" -ge 1005 ]] || return 0

  if ! pistorm_modern_p96_experiment_enabled; then
    echo "==> P96: modern VideoCore.card detected (>= 1.5) with rtg.library v${user_rtg_ver}"
    echo "==> P96: conservative v40-compatible core remains default on PiStorm"
    echo "==> P96: supply an iComp P96 archive to use the modern PiStorm core"
    return 0
  fi

  echo "==> P96: modern VideoCore.card detected (>= 1.5) with rtg.library v${user_rtg_ver}"
  if [[ -n "${P96_ICOMP_ARCHIVE:-}" ]]; then
    echo "==> P96: PiStorm iComp archive supplied; restoring user-supplied modern core"
  else
    echo "==> P96: explicit modern PiStorm mode enabled; restoring user-supplied modern core"
  fi
  echo "==> P96: keeping VC4_LEGACY_ID on PiStorm until modern-core boot is proven on real hardware"

  export P96_TRUST_ARCHIVE_ABI=1
  stage_p96_icomp_archive "$sys_root"
  reapply_vc4_legacy_id_tooltype "$sys_root"

  if [[ "$saved_trust" == "__unset__" ]]; then
    unset P96_TRUST_ARCHIVE_ABI
  else
    export P96_TRUST_ARCHIVE_ABI="$saved_trust"
  fi
}

# 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/"
}


# --- 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
      "$AMIGA_TOOLS" regex-replace --case-insensitive "$tmp_cfg" '^[ \t]*initramfs[ \t]+.*' 'initramfs kick.rom'
    else
      printf "\ninitramfs kick.rom\n" >> "$tmp_cfg"
    fi
  fi

  # Defensive: comment out a known-bad corrupted line seen in some builds
  "$AMIGA_TOOLS" regex-replace "$tmp_cfg" '^[ \t]*r now\.' '# r now.' 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"
    # v1.1.x: Unicam is initialized by the Pi firmware via dtoverlay; inject into config.txt
    if [[ "${EMU68_BUNDLE_FORMAT:-v1}" == "v2" ]]; then
      if ! grep -q 'dtoverlay=unicam' "$tmp_cfg"; then
        local _boot_token=""
        [[ "${UNICAM_AUTOSTART:-1}" != "0" ]] && _boot_token="boot,"
        local _type_suffix=""
        [[ -n "${UNICAM_TYPE:-}" ]] && _type_suffix=",type=${UNICAM_TYPE}"
        local _unicam_overlay="dtoverlay=unicam,${_boot_token}${UNICAM_MODE:-smooth}${_type_suffix}"
        "$AMIGA_TOOLS" insert-before-line "$tmp_cfg" '^include emu68imager\.cfg' "${_unicam_overlay}"
      fi
    fi
    # Both v1.x and v1.1.x: setvar in emu68imager.cfg for Emu68 internal state
    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
}

# ===========================================================================
# MAIN PIPELINE
# ===========================================================================
# -----------------------------------------------------------------------------
# Parse flags
# -----------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    --total-gb)        TOTAL_GB="${2:?}"; shift 2;;
    --total-bytes)     TOTAL_BYTES_OVERRIDE="${2:?}"; shift 2;;
    --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;;
    --amiga-tools)      AMIGA_TOOLS="${2:?}"; shift 2;;
    --transfer-dir)     TRANSFER_DIR="${2:?}"; shift 2;;
    --transfer-partition) TRANSFER_PARTITION="${2:?}"; shift 2;;
    --partition-content-map) PARTITION_CONTENT_MAP="${2:?}"; shift 2;;
    --adf-dir)          ADF_DIR="${2:?}"; shift 2;;
    --emu68-boot-url)    EMU68_BOOT_URL="${2:?}"; shift 2;;
    --emu68-boot-mode)   EMU68_BOOT_MODE="${2:?}"; shift 2;;
    --emu68-prerelease)  EMU68_PRERELEASE="${2:?}"; shift 2;;
    --icon-set)         ICON_SET="${2:?}"; shift 2;;
    --packages)         PACKAGES="${2:?}"; shift 2;;
    --locales)          LOCALES="${2:?}"; shift 2;;
    --csv-dir)          CSV_DIR="${2:?}"; shift 2;;
    --aminet-dir)       AMINET_DIR="${2:?}"; shift 2;;
    --packages-dir)     AMINET_DIR="${2:?}"; IBROWSE_DIR="${2:?}"; shift 2;;  # unified staging dir (Aminet + direct + user packages)
    --aminet-rules-csv) AMINET_RULES_CSV="${2:?}"; shift 2;;
    --download-only)    shift 2;;  # removed — no longer used
    --p96-icomp-archive) P96_ICOMP_ARCHIVE="${2:?}"; shift 2;;
    --pistorm-modern-p96) PISTORM_MODERN_P96_MODE="${2:?}"; shift 2;;
    --has-floppy)       HAS_FLOPPY="${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)        shift 2;;
    --unicam-enable)    UNICAM_ENABLE="${2:?}"; shift 2;;
    --unicam-mode)      UNICAM_MODE="${2:?}"; shift 2;;
    --unicam-type)      UNICAM_TYPE="${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;;
    --govd-key)         GOVD_KEY="${2:?}"; shift 2;;
    --build-manifest)   BUILD_MANIFEST="${2:?}"; shift 2;;
    --build-plan)       BUILD_PLAN="${2:?}"; shift 2;;
    --icon-pass2-mode)  ICON_PASS2_MODE="${2:?}"; shift 2;;
    --blank-amiga-partitions) BLANK_AMIGA_PARTITIONS="${2:?}"; shift 2;;
    --include-wbdock|--include-igame|--include-lumiftp|--include-amigaamp|--include-riva|--include-bass|--include-fotaq|--include-lure) shift;; # removed in Stage C; IDs now travel via --packages
    --whdload-kickstarts-dir) WHDLOAD_KICKSTARTS_DIR="${2:?}"; shift 2;;
    --imp3-reg)             IMP3_REG="${2:?}"; shift 2;;
    --imp3-reg-file)        IMP3_REG_FILE="${2:?}"; shift 2;;
    --boing-bag-1)          BOING_BAG_1="${2:?}"; shift 2;;
    --boing-bag-2)          BOING_BAG_2="${2:?}"; shift 2;;
    --partition)        partition_layout_add_spec "${2:?}"; shift 2;;
    *) die "Unknown argument: $1";;
  esac
done

load_build_plan "${BUILD_PLAN:-}"

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

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

# -----------------------------------------------------------------------------
# Normalize filesystem token for hst-imager
# -----------------------------------------------------------------------------
# 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
  if [[ "$BLANK_AMIGA_PARTITIONS" -eq 1 && "$HST_FS" == "Pfs3" ]]; then
    :
  elif [[ "$BLANK_AMIGA_PARTITIONS" -eq 1 ]]; then
    die "Blank FFS images need --adf-dir or an ISO path so FastFileSystem can be extracted. Use PFS3 to build without media."
  else
    die "No input media available. Provide --adf-dir (preferred) or an ISO path as 3rd positional argument."
  fi
fi

case "$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


# --- N-partition support: populate partition arrays from --partition args or legacy fallback ---
partition_layout_prepare "SDH" 0

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

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

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

echo "==> Layout:"
echo "    Total: ${TOTAL_GB} GB (${DISK_SIZE_BYTES} bytes)"
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]} block=${PART_BLOCKSIZES[$_i]} boot=${PART_BOOTABLE[$_i]} pri=${PART_BOOTPRI[$_i]}"
done
echo "    FS:    ${AMIGA_FS} (dostype=${HST_FS})"
echo "    ADF:   ${ADF_DIR:-<auto-detect from ISO/ADF>}"
echo "    ISO:   ${ISO_PATH:-<none>}"
echo "    Boot:  mode=${EMU68_BOOT_MODE} url=${EMU68_BOOT_URL:-<auto>} prerelease=${EMU68_PRERELEASE}"
echo "    Icons: ${ICON_SET} (source ADF selected automatically: 4ColorIcons or GlowIcons)"
echo "    Locales: ${LOCALES:-<all>}"
echo "    Pkgs:  ${PACKAGES:-<none>}"
echo "    Blank partitions: ${BLANK_AMIGA_PARTITIONS}"
echo "    P96:   ${P96_ICOMP_ARCHIVE:-<none>}"
echo "    Debug: ${DEBUG} (export-dir=${DEBUG_EXPORT_DIR:-<img directory>})"
echo "    Tools: amiga-tools=${AMIGA_TOOLS:-<not set>}"

# -----------------------------------------------------------------------------
# Stage + ISO mount
# -----------------------------------------------------------------------------
STAGE="$(mktemp -d "/tmp/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)"
  _already_mounted="$("${AMIGA_TOOLS}" hdiutil-find-mount "$ISO_PATH" 2>/dev/null || true)"
  if [[ -n "$_already_mounted" ]]; then
    echo "==> ISO already mounted at $_already_mounted (reusing)"
    MNT="$_already_mounted"
  else
    hdiutil attach "$ISO_PATH" -readonly -nobrowse -mountpoint "$MNT" >/dev/null
    ISO_MOUNTED=1
  fi
  ISO_ROOT="$MNT"
else
  echo "==> No ISO provided; using ADF-only mode"
fi

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

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

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

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

    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 "==> PROGRESS 1/12 Creating blank image"
echo "==> 1) Create blank image"
mkdir -p "$(dirname "$IMG")"
if [[ -e "$IMG" ]]; then
  echo "==> Removing existing image: $IMG"
  rm -f "$IMG"
fi

FAT_ROOT="$IMG/mbr/1"
RDB="$IMG/mbr/2"

# PiStorm preset geometry (hst-imager parity): FAT32 boot partition at
# LBA 2048 ending at the 1 GiB boundary, RDB slice directly after it.
PISTORM_FAT_LBA=2048
PISTORM_FAT_SECTORS=$(( 1073741824 / 512 - 2048 + 1 ))   # 2,095,105
PISTORM_RDB_LBA=$(( 1073741824 / 512 + 1 ))              # 2,097,153

if [[ "${DISK_ENGINE:-hst}" == "amigadiskkit" ]]; then
  # Native engine: image + MBR + RDB + partitions are created together in
  # step 3 (rdb-build --mbr), followed by native FAT32 format and handler
  # registration. Nothing to do yet.
  echo "==> 1) [amigadiskkit] image creation deferred to step 3 (rdb-build --mbr)"
  echo "==> PROGRESS 2/12 Formatting image (deferred)"
  echo "==> 2) [amigadiskkit] FAT32 + RDB formatting deferred to step 3"
else
  run "$HST_IMAGER" blank "$IMG" "$DISK_SIZE_BYTES"

  # ---------------------------------------------------------------------------
  # 2) Format PiStorm preset (Windows-aligned)
  # ---------------------------------------------------------------------------
  echo "==> PROGRESS 2/12 Formatting image"
  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

  # 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

  if ! fs_dir "$FAT_ROOT"; 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

  # The `format` command registers the filesystem internally using hst-imager's
  # own token (PFS3), not the correct Amiga dostype (PDS3). Explicitly register
  # PDS3 so that partitioning succeeds. This mirrors what the Classic and MiSTer
  # scripts do with an explicit fs registration before partitioning.
  if partition_layout_uses_fs "Pfs3"; then
    run "$HST_IMAGER" rdb fs add "$RDB" "$PFS3_AIO" PDS3
  fi
fi

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

if [[ "${DISK_ENGINE:-hst}" == "amigadiskkit" ]]; then
  # Native engine: compute usable RDB-slice bytes from the geometry directly
  # (the image does not exist yet — rdb-build creates it below).
  _native_pure_rdb_usable_bytes $(( DISK_SIZE_BYTES - PISTORM_RDB_LBA * 512 ))
else
  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"
fi

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

# When PFS3 is the primary filesystem, force ALL partitions to PFS3.
# 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

# Auto-upgrade large FFS data partitions to FfsDC (DOS7).
upgrade_large_ffs_data_partitions

if [[ "${DISK_ENGINE:-hst}" == "amigadiskkit" ]]; then
  # -------------------------------------------------------------------------
  # Native engine: create image + MBR + RDB + partitions in one rdb-build,
  # then FAT32-format the boot partition and register filesystem handlers.
  # -------------------------------------------------------------------------
  echo "==> [amigadiskkit] Building image, MBR, FAT32 boot partition and RDB layout"
  _native_build_part_args "$NUM_PARTS"
  run "$AMIGA_TOOLS" disk rdb-build "$IMG" "$DISK_SIZE_BYTES" --mbr \
      --fat-lba "$PISTORM_FAT_LBA" --fat-sectors "$PISTORM_FAT_SECTORS" \
      --rdb-lba "$PISTORM_RDB_LBA" "${NATIVE_PART_ARGS[@]}"
  run "$AMIGA_TOOLS" disk fat-format "$IMG" EMU68BOOT --mbr-index 1

  # Register filesystem handlers in the RDB (the hst path embeds them via
  # 'format' + 'rdb fs add'; rdb-fs-add is idempotent).
  if [[ "$HST_FS" == "Pfs3" ]]; then
    run "$AMIGA_TOOLS" disk rdb-fs-add "$IMG" "$PFS3_AIO" PDS3
  else
    run "$AMIGA_TOOLS" disk rdb-fs-add "$IMG" "$FFS_PATH" "$HST_FS" --name FastFileSystem
    if partition_layout_uses_fs "Pfs3"; then
      run "$AMIGA_TOOLS" disk rdb-fs-add "$IMG" "$PFS3_AIO" PDS3
    fi
  fi

  # Sanity: FAT root and RDB must be readable through the native engine.
  "$AMIGA_TOOLS" disk fat dir "$IMG" / >/dev/null \
    || die "PiStorm native bootstrap failed: FAT partition not accessible"
  "$AMIGA_TOOLS" disk rdb-info "$IMG" >/dev/null \
    || die "PiStorm native bootstrap failed: RDB slice not accessible"

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

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

  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

  # 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 "${PART_BLOCKSIZES[$_i]}" --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

  # Verify every partition is mountable after format.
  #
  # Two known hst-imager v1.5.564 bugs are handled here:
  #
  # Bug 1 — FFS silent boot-block corruption (> 2 GiB, FFS/DOS3):
  #   rdb part format reports Done but writes 00000000 to the boot block for FFS
  #   partitions > 2 GiB. Fixed upstream by upgrade_large_ffs_data_partitions
  #   (auto-upgrades to DOS7). Caught here as a belt-and-suspenders check.
  #
  # Bug 2 — uint32 overflow in FastFileSystemHelper.Mount (> 4 GiB partition start):
  #   fs dir/copy computes bootBlockOffset = lowCyl * surfaces * blocksPerTrack * blockSize
  #   in uint32, which wraps for partitions starting beyond 4 GiB (e.g. a 3rd large
  #   partition in a 28+ GiB image). The format itself uses int64 and writes the
  #   correct boot block; only the mount/dir/copy commands are affected.
  #   Workaround: when fs dir fails with 00000000, fall back to a raw byte check
  #   at the disk-absolute boot block offset (immune to the overflow).
  #
  # Get disk-absolute start of the RDB slice (MBR partition 2 in PiStorm images).
  _rdb_disk_start=$("$HST_IMAGER" info "$IMG" 2>/dev/null \
    | awk -F'|' '/PiStorm RDB/ {
        s=$6; gsub(/[[:space:]]+/, "", s)
        if (s ~ /^[0-9]+$/) { print s; exit }
      }')
  _rdb_disk_start="${_rdb_disk_start:-0}"
  for _i in $(seq 0 $((NUM_PARTS - 1))); do
    _pn="${PART_PNS[$_i]}"
    _vpath="$IMG/mbr/2/rdb/$_pn"
    if fs_dir "$_vpath"; then
      echo "==> Partition ${PART_NAMES[$_i]} (${PART_LABELS[$_i]}) post-format: OK"
      continue
    fi
    # fs dir failed — try raw boot block byte check (bypasses uint32 overflow bug)
    _slice_start=$("$HST_IMAGER" info "$RDB" 2>/dev/null \
      | awk -F'|' -v pn="$_pn" '
          /RDB/ {
            n=$4; gsub(/[[:space:]]+/, "", n)
            if (n == pn) {
              s=$6; gsub(/[[:space:]]+/, "", s)
              if (s ~ /^[0-9]+$/) { print s; exit }
            }
          }')
    if [[ -z "$_slice_start" ]]; then
      echo "==> ERROR: Partition ${PART_NAMES[$_i]} (pn $_pn, ${PART_LABELS[$_i]}) failed post-format check." >&2
      echo "==>   hst-imager fs dir failed and start offset not found in info output." >&2
      echo "==>   FS type: ${PART_FSTYPES[$_i]} → dostype $(part_fs_to_dostype "${PART_FSTYPES[$_i]}" "${PART_BOOTABLE[$_i]}")" >&2
      die "Post-format check failed for partition ${PART_NAMES[$_i]}"
    fi
    _disk_abs=$(( _rdb_disk_start + _slice_start ))
    _dd_block=$(( _disk_abs / 512 ))
    _dd_off=$(( _disk_abs % 512 ))
    _raw_magic=$(dd if="$IMG" bs=512 skip="$_dd_block" count=1 2>/dev/null \
      | dd bs=1 skip="$_dd_off" count=4 2>/dev/null \
      | od -A n -t x1 \
      | tr -d ' \n')
    _dostype=$(part_fs_to_dostype "${PART_FSTYPES[$_i]}" "${PART_BOOTABLE[$_i]}")
    case "$_dostype" in
      DOS3) _expected_hex="444f5303" ;;
      DOS7) _expected_hex="444f5307" ;;
      PDS3) _expected_hex="50445303" ;;
      *)    _expected_hex="" ;;
    esac
    if [[ -n "$_expected_hex" && "$_raw_magic" == "$_expected_hex" ]]; then
      echo "==> Partition ${PART_NAMES[$_i]} (${PART_LABELS[$_i]}) post-format: OK (raw boot block check)"
      echo "==>   (hst-imager fs dir skipped: uint32 overflow for partition at disk byte $_disk_abs)"
      continue
    fi
    echo "==> ERROR: Partition ${PART_NAMES[$_i]} (pn $_pn, ${PART_LABELS[$_i]}) failed post-format check." >&2
    echo "==>   FS type: ${PART_FSTYPES[$_i]} → dostype $_dostype (expected: ${_expected_hex:-unknown})" >&2
    echo "==>   Boot block at disk byte $_disk_abs: 0x${_raw_magic}" >&2
    echo "==>   For FFS partitions > 2 GiB, upgrade_large_ffs_data_partitions should prevent this." >&2
    die "Post-format check failed for partition ${PART_NAMES[$_i]}"
  done

  # Bug 1 guard: explicitly verify boot partition FFS boot block checksum.
  #
  # hst-imager's `fs dir` can succeed even when the boot block checksum is zero
  # (hst-imager is lenient about its own written checksums). AmigaOS FFS on real
  # hardware rejects zero-checksum boot blocks: "Not a DOS disk in device SDH0".
  #
  # upgrade_large_ffs_data_partitions() skips bootable partitions, so SDH0 stays
  # on DOS3 even when it rounds above 2 GiB (default 2 GiB → ~2.001 GiB after
  # cylinder-alignment), which is the Bug 1 trigger range.
  _bug1_pn="${PART_PNS[0]}"
  _bug1_slice_start=$("$HST_IMAGER" info "$RDB" 2>/dev/null \
    | awk -F'|' -v pn="$_bug1_pn" '/RDB/ {
        n=$4; gsub(/[[:space:]]+/, "", n)
        if (n == pn) {
          s=$6; gsub(/[[:space:]]+/, "", s)
          if (s ~ /^[0-9]+$/) { print s; exit }
        }
      }')
  if [[ -n "$_bug1_slice_start" ]]; then
    _bug1_abs=$(( _rdb_disk_start + _bug1_slice_start ))
    _bug1_blk=$(( _bug1_abs / 512 ))
    _bug1_cksum=$(dd if="$IMG" bs=512 skip="$_bug1_blk" count=1 2>/dev/null \
      | dd bs=1 skip=4 count=4 2>/dev/null \
      | od -A n -t x1 | tr -d ' \n')
    _bug1_rootptr=$(dd if="$IMG" bs=512 skip="$_bug1_blk" count=1 2>/dev/null \
      | dd bs=1 skip=8 count=4 2>/dev/null \
      | od -A n -t x1 | tr -d ' \n')
    if [[ "$_bug1_cksum" == "00000000" && "$_bug1_rootptr" == "00000000" ]]; then
      echo "==> ERROR: Boot partition ${PART_NAMES[0]} (${PART_LABELS[0]}) has a corrupt boot block." >&2
      echo "==>   FFS checksum and root-block pointer are both zero — hst-imager Bug 1." >&2
      echo "==>   This silently corrupts DOS3 partitions that exceed 2 GiB (including the" >&2
      echo "==>   default 2 GiB SDH0, which rounds to ~2.001 GiB after cylinder alignment)." >&2
      echo "==>   AmigaOS shows 'Not a DOS disk in device ${PART_NAMES[0]}' on first boot." >&2
      echo "==>   Fix: Settings → Debug → Disk Engine → Native (recommended)." >&2
      echo "==>   Alt: reduce ${PART_NAMES[0]} size to 1.8 GiB or below in the UI." >&2
      die "Boot block corruption (hst-imager Bug 1) in ${PART_NAMES[0]}"
    fi
  fi
fi

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

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

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

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

  echo "==> Applying partition content map"

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

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

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

# -----------------------------------------------------------------------------
# 4) Populate FAT (kick.rom + Emu68 boot bundle)
# -----------------------------------------------------------------------------
echo "==> PROGRESS 4/12 Populating FAT boot partition"
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 zips (Assets boot bundle incomplete)"

  # Download and merge all three PiStorm variant zips (pistorm, pistorm16,
  # pistorm32lite). Each zip contains its own kernel binary plus the shared
  # Pi firmware / dtb files (identical across zips). Merging gives us all
  # three kernels so config.txt can auto-select the correct one per
  # PiStorm hardware at runtime. Classic PiStorm (A500/A600) requires
  # Emu68-pistorm; aliasing pistorm32lite to that filename is NOT valid
  # (different GPIO pinout, hangs at splash).
  ZIP_URLS="$(resolve_emu68_pistorm_all_zip_urls)"
  [[ -n "$ZIP_URLS" ]] || die "Could not resolve any Emu68 PiStorm zip URL from GitHub API. Set --emu68-boot-url explicitly."
  # Recover EMU68_RELEASE_TAG from the temp file written inside the subshell
  if [[ -z "$EMU68_RELEASE_TAG" && -f "$STAGE/emu68-release-tag.txt" ]]; then
    EMU68_RELEASE_TAG="$(cat "$STAGE/emu68-release-tag.txt" 2>/dev/null || true)"
    echo "==> Emu68 release tag: ${EMU68_RELEASE_TAG:-unknown}"
  fi

  EMU68_DIR="$STAGE/emu68"
  mkdir -p "$EMU68_DIR"

  variant_index=0
  while IFS= read -r ZIP_URL; do
    [[ -n "$ZIP_URL" ]] || continue
    variant_index=$((variant_index + 1))
    echo "==> Emu68 zip ($variant_index): $ZIP_URL"

    EMU68_ZIP="$STAGE/emu68-${variant_index}.zip"
    VARIANT_DIR="$EMU68_DIR/v${variant_index}"
    mkdir -p "$VARIANT_DIR"

    if ! curl -fL --retry 3 --retry-delay 2 -o "$EMU68_ZIP" "$ZIP_URL"; then
      echo "==> WARN: failed to download $ZIP_URL — continuing with other variants"
      continue
    fi
    unzip -q "$EMU68_ZIP" -d "$VARIANT_DIR" || {
      echo "==> WARN: failed to unzip $EMU68_ZIP — continuing with other variants"
      continue
    }

    payload="$VARIANT_DIR"
    top_dirs="$(find "$VARIANT_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
    top_files="$(find "$VARIANT_DIR" -mindepth 1 -maxdepth 1 -type f | wc -l | tr -d ' ')"
    if [[ "$top_dirs" -eq 1 && "$top_files" -eq 0 ]]; then
      payload="$(find "$VARIANT_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
  done <<< "$ZIP_URLS"
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

# Detect boot bundle format:
#   v1 = v1.0.x  — uncompressed kernels (Emu68-pistorm32lite, Emu68-pistorm), GPIO detection in config.txt
#   v2 = v1.1.x+ — gzip-compressed kernels (Emu68-pistorm.gz), single unified binary, no GPIO detection
EMU68_BUNDLE_FORMAT="v1"
if find "$BOOT_STAGE" -maxdepth 1 -iname "Emu68-pistorm*.gz" 2>/dev/null | grep -q .; then
  EMU68_BUNDLE_FORMAT="v2"
  echo "==> Detected Emu68 v1.1.x boot bundle (gzip kernels, unified binary)"
else
  echo "==> Detected Emu68 v1.0.x boot bundle (uncompressed kernels, GPIO detection)"
fi

USED_ASSETS_CONFIG_TEMPLATE=0
ASSETS_CONFIG="$ASSETS_DIR/AmigaFiles/EMU68Boot/config.txt"

if [[ "$EMU68_BUNDLE_FORMAT" == "v2" ]]; then
  # v1.1.x: generate a clean config.txt without GPIO detection.
  # The unified Emu68-pistorm.gz binary handles PS32lite and PS16 automatically.
  echo "==> Generating v1.1.x config.txt (no GPIO detection)"
  USED_ASSETS_CONFIG_TEMPLATE=1   # suppress global initramfs replacement in write_phase2_config

  # Pick the main kernel: prefer Emu68-pistorm.gz (unified), fall back to any .gz
  EMU68_V2_KERNEL="Emu68-pistorm.gz"
  if ! find "$BOOT_STAGE" -maxdepth 1 -iname "Emu68-pistorm.gz" 2>/dev/null | grep -q .; then
    EMU68_V2_KERNEL="$(find "$BOOT_STAGE" -maxdepth 1 -iname "Emu68-pistorm*.gz" 2>/dev/null | head -n1 | xargs basename 2>/dev/null || echo "Emu68-pistorm.gz")"
  fi

  # Build video mode block
  if [[ -n "$SCREEN_WIDTH" && -n "$SCREEN_HEIGHT" ]]; then
    V2_HDMI="hdmi_group=2
hdmi_mode=87
hdmi_cvt=${SCREEN_WIDTH} ${SCREEN_HEIGHT} ${SCREEN_FPS:-60}
disable_overscan=1"
  else
    V2_HDMI="hdmi_group=0
disable_overscan=1"
  fi

  cat > "$BOOT_STAGE/config.txt" <<EOF
# Generated by Amiga Imager (Emu68 v1.1.x)
kernel=$EMU68_V2_KERNEL
disable_splash=1
arm_64bit=1
boot_delay=0
avoid_warnings=2
force_turbo=1
over_voltage=4
arm_freq=1800
hdmi_force_hotplug=1
gpu_mem=32
$V2_HDMI
initramfs kick.rom
include emu68imager.cfg
EOF
  make_lf_only "$BOOT_STAGE/config.txt"

elif [[ -f "$ASSETS_CONFIG" ]]; then
  # v1.0.x with Assets template: GPIO-based detection for PS32lite / PS16 / classic PiStorm
  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
  "$AMIGA_TOOLS" replace-line-prefix "$BOOT_STAGE/config.txt" '[ROMPATH]' 'initramfs kick.rom'

  # Replace [VIDEOMODES] placeholder with screen mode settings
  if [[ -n "$SCREEN_WIDTH" && -n "$SCREEN_HEIGHT" ]]; then
    VIDMODE_BLOCK="hdmi_group=2
hdmi_mode=87
hdmi_cvt=${SCREEN_WIDTH} ${SCREEN_HEIGHT} ${SCREEN_FPS:-60}
disable_overscan=1"
  else
    VIDMODE_BLOCK="hdmi_group=0
disable_overscan=1"
  fi
  "$AMIGA_TOOLS" replace-line-prefix "$BOOT_STAGE/config.txt" '[VIDEOMODES]' "${VIDMODE_BLOCK}"

  # 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

# v1.0.x: ensure all three kernel names are present (GPIO selector references all three).
# v1.1.x: unified binary, no aliases needed.
if [[ "$EMU68_BUNDLE_FORMAT" == "v1" ]]; then
  ensure_boot_bundle_kernel_aliases "$BOOT_STAGE"
fi

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"



write_phase2_config "$FAT_ROOT"

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

# -----------------------------------------------------------------------------
# 5) Populate SYS: from ADFs (Windows-style merge)
# -----------------------------------------------------------------------------
echo "==> PROGRESS 5/12 Extracting AmigaOS from ADFs"
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 "$SYS_HOST" "${ASSETS_DIR:-}"
install_rtg_backdrop_png "$SYS_HOST" "${ADF_SRC:-}" "$SCREEN_WIDTH" "$SCREEN_HEIGHT" "$DETECTED_WB_VERSION"

# --- 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.
# IMPORTANT: Only copy .info when none exists in Tools/ — the icon set ADF
# (GlowIcons/4ColorIcons) places the proper styled icon there. Overwriting it
# with the HDSetup ADF's generic icon would lose the styled version.
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
  if [[ -f "$SYS_HOST/HDToolBox.info" && ! -f "$SYS_HOST/Tools/HDToolBox.info" ]]; then
    cp -f "$SYS_HOST/HDToolBox.info" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
  fi
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
  if [[ -f "$SYS_HOST/HDTools.info" && ! -f "$SYS_HOST/Tools/HDToolBox.info" ]]; then
    cp -f "$SYS_HOST/HDTools.info" "$SYS_HOST/Tools/HDToolBox.info" 2>/dev/null || true
  fi
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
      # Only copy .info if none exists (preserve styled icon from GlowIcons/ColorIcons ADF)
      if [[ ! -f "$SYS_HOST/Tools/HDToolBox.info" ]]; then
        _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
      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
    # Only copy .info if none exists (preserve styled icon from GlowIcons/ColorIcons ADF)
    if [[ ! -f "$SYS_HOST/Tools/HDToolBox.info" ]]; then
      _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
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 — strip setup-media-only utilities but keep Installer (general-purpose
# AmigaOS utility used by any package with an install script; removing it breaks DefaultTool=Installer)
for _junk in \
  ActivateCDROM ActivateCDROM.info \
  HDSetup HDSetup.info HDTools HDTools.info \
  Install.info \
  SYStoInstallDisk SYStoInstallDisk.info \
  CI CI.info \
  ; do
  [[ -e "$SYS_HOST/$_junk" ]] && { rm -rf "$SYS_HOST/$_junk"; _rm_count=$((_rm_count+1)); }
done
# Move root-level Installer binary to System/Installer (conventional location).
# WB 3.2 install media places Installer at SYS: root; relocate it so the
# INSTALLER: assign can consistently target SYS:System.
if [[ -f "$SYS_HOST/Installer" ]] && [[ ! -f "$SYS_HOST/System/Installer" ]]; then
  mkdir -p "$SYS_HOST/System"
  mv "$SYS_HOST/Installer" "$SYS_HOST/System/Installer"
  [[ -f "$SYS_HOST/Installer.info" ]] && mv "$SYS_HOST/Installer.info" "$SYS_HOST/System/Installer.info"
  echo "==> Relocated Installer → System/Installer"
fi
# 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
# Raw .adf files at SYS: root (from ADF extraction — contents already merged)
for _radf in "$SYS_HOST"/*.adf "$SYS_HOST"/*.ADF; do
  [[ -f "$_radf" ]] && { rm -f "$_radf"; _rm_count=$((_rm_count+1)); }
done
for _radfi in "$SYS_HOST"/*.adf.info "$SYS_HOST"/*.ADF.info; do
  [[ -f "$_radfi" ]] && { rm -f "$_radfi"; _rm_count=$((_rm_count+1)); }
done
# Install/ directory contains raw ADFs from install media — not needed on final image
if [[ -d "$SYS_HOST/Install" ]]; then
  rm -rf "$SYS_HOST/Install"
  _rm_count=$((_rm_count+1))
fi
[[ -f "$SYS_HOST/Install.info" ]] && { rm -f "$SYS_HOST/Install.info"; _rm_count=$((_rm_count+1)); }
# 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"
sanitize_non_ascii_filenames "$SYS_HOST"

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 "==> PROGRESS 6/12 Overlaying Assets"
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 ! fs_dir "$_p"; 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"
if [[ "$BACKDROP_ENABLED" == "1" && "$_RTG_BACKDROP_PNG_INSTALLED" == "1" ]]; then
  write_wbpattern_backdrop "$SYS_ROOT" "Sys:Prefs/Presets/Backdrops/backdrop.png"
fi

# Set keyboard layout based on selected locale (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 ! fs_dir "$SYS_ROOT/System/CLICon" \
   && ! fs_dir "$SYS_ROOT/System/CLIcon"; then
  if fs_dir "$SYS_ROOT/System/CLI"; then
    local _cli_tmp="$STAGE/.cli-copy-$$.$RANDOM"
    if fs_pull_file "$SYS_ROOT/System/CLI" "$_cli_tmp" 2>/dev/null; then
      fs_copy_file "$_cli_tmp" "$SYS_ROOT/System/CLIcon" 2>/dev/null || true
    fi; rm -f "$_cli_tmp" 2>/dev/null || true
    echo "==> Copied System/CLI -> System/CLIcon (WB3.2 LD fallback)"
  fi
fi

# mecho (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"

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

# icon.library 46.4 — 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 ! fs_dir "$SYS_ROOT/Libs/icon.library"; 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 fs_dir "$SYS_ROOT/Libs/muimaster.library"; 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 ! fs_dir "$SYS_ROOT/Libs/bgui.library"; then
    if fs_dir "$SYS_ROOT/Programs/AHI/BGUI/bgui.library"; then
      _bgui_tmp="$STAGE/bgui.library.$$"
      if fs_pull_file "$SYS_ROOT/Programs/AHI/BGUI/bgui.library" "$_bgui_tmp" >/dev/null 2>&1; then
        fs_copy_file "$_bgui_tmp" "$SYS_ROOT/Libs/bgui.library"
        echo "==> Installed bgui.library to LIBS: (for AHI_BGUI)"
        rm -f "$_bgui_tmp" 2>/dev/null || true
      fi
    fi
  fi
fi
# Pull the chosen AHI prefs binary to host, then push as both "AHI" and "AHI.MWB"
if fs_dir "$SYS_ROOT/Prefs/$_ahi_prefs_src"; then
  _ahi_tmp="$STAGE/ahi-prefs-bin.$$"
  if fs_pull_file "$SYS_ROOT/Prefs/$_ahi_prefs_src" "$_ahi_tmp" >/dev/null 2>&1; then
    fs_copy_file "$_ahi_tmp" "$SYS_ROOT/Prefs/AHI"
    echo "==> Copied Prefs/$_ahi_prefs_src -> Prefs/AHI"
    fs_copy_file "$_ahi_tmp" "$SYS_ROOT/Prefs/AHI.MWB"
    echo "==> Copied Prefs/$_ahi_prefs_src -> Prefs/AHI.MWB"
    rm -f "$_ahi_tmp" 2>/dev/null || true
  fi
fi

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

# --- 6i) Set post-overlay icons borderless (covers base WB icons before package installs) ---
pkg_time_start
set_coloricons_borderless_image "$SYS_ROOT" "Tools" "System" "Prefs" "Programs" "PiStorm" "Emu68" "Utilities" "WBStartup" "Devs" "Storage"
pkg_time_done "set_coloricons_borderless (pre-pkg)"

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

#
# -----------------------------------------------------------------------------
# Persist a tiny note for debugging on the Amiga side
TMP_ADF_NOTE="$STAGE/amigaimager-adf-staged.txt"
if [[ "$IS_39_ISO" -eq 1 ]]; then
  printf "source:OS3.9-CD-ISO\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
}

# -----------------------------------------------------------------------------
echo "==> PROGRESS 8/12 Installing optional software"
# 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
# -----------------------------------------------------------------------------
# SD0Pi4/SD0Pi3 DOSDrivers (bundled in Assets/Devs/DOSDrivers/) are overlaid
# unconditionally. Pi4vsPi3_Pistorm renames the correct one to SD0 at first boot.
# Both drivers specify FileSystem = L:fat95, so fat95 must always be present.
# EMU68BOOT: is an assign pointing to SD0: written by amigaimager-assigns.
echo "==> EMU68BOOT: installing fat95 (required by SD0 DOSDriver, both Pi3 and Pi4)"
ensure_fat95_in_sys "$SYS_ROOT"

# -----------------------------------------------------------------------------
#
# 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 ! fs_dir "$SYS_ROOT/S"; then
  die "Failed to create/access SYS:S at $SYS_ROOT/S"
fi
if ! fs_dir "$SYS_ROOT/S/OneTimeRunWB"; then
  die "Failed to create/access SYS:S/OneTimeRunWB at $SYS_ROOT/S/OneTimeRunWB"
fi

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

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

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

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

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

# -----------------------------------------------------------------------------
# 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
    lha_base="$(basename "$lha_file")"
    # IBrowse/AmiSSL/WBDock have dedicated installers; skip them here.
    is_dedicated_external_archive "$lha_base" && continue
    AMINET_FILES+=("$lha_file")
    case "$(printf '%s' "$lha_base" | tr '[:upper:]' '[:lower:]')" in
      lha_68040.lha|lha_68020.lha|lha_68k.lha) LHA_READY=1 ;;
    esac
  done

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

  # LhA is mandatory (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

    # mpega.library — CPU-aware install: PiStorm/Emu68 is always 68060+FPU.
    if echo "$lha_base" | grep -qi "^mpega_library\.lha$"; then
      echo "  -> processing Aminet: $lha_base (cpu_class=fpu)"
      pkg_time_start
      install_mpega_library "$SYS_ROOT" "$lha_file" "fpu"
      pkg_time_done "install_mpega_library"
      AMINET_BUILDTIME+=("$lha_base")
      continue
    fi

    # ScummVM — PiStorm/Emu68 = always 68060 (Phase 6: dispatch via common.sh wrapper)
    if is_scummvm_archive "$lha_base"; then
      install_pkg_scummvm "$SYS_ROOT" "$lha_file" "$lha_base" "060" && AMINET_BUILDTIME+=("$lha_base")
      continue
    fi

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

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

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


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

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

    # 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"
fixup_ahi_prefs "$SYS_ROOT"

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

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

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

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

# Belt-and-suspenders: force-install the Emu68 Picasso96Settings bundled in
# Assets. The Assets overlay (step 6) should have copied it, and
# stage_p96_icomp_archive explicitly avoids touching Picasso96Settings so
# the Assets version wins. But on some user-supplied P96 archives (iComp
# v43) the fallback path prints "Picasso96Install directory not found"
# and the user ends up with an image where P96Prefs shows the VideoCore
# card but no screen modes (user has to create them manually). Guarantee
# the correct board/mode definitions are on the image by copying
# unconditionally here. PiStorm builds always use VideoCore RTG, so
# overwriting is safe.
ASSETS_P96_SETTINGS="$ASSETS_DIR/AmigaFiles/System/Devs/Picasso96Settings"
if [[ -f "$ASSETS_P96_SETTINGS" ]]; then
  _p96_settings_size="$(stat -f %z "$ASSETS_P96_SETTINGS" 2>/dev/null || stat -c %s "$ASSETS_P96_SETTINGS" 2>/dev/null || echo 0)"
  echo "==> P96: installing Assets Picasso96Settings with VideoCore board+modes (${_p96_settings_size} bytes)"
  ensure_img_dir "$SYS_ROOT/Devs"
  fs_copy_file "$ASSETS_P96_SETTINGS" "$SYS_ROOT/Devs/Picasso96Settings"
else
  echo "==> WARN: Assets Devs/Picasso96Settings not found at $ASSETS_P96_SETTINGS"
  echo "==>   RTG screen modes will be missing from P96Prefs; user must create them manually"
fi

# Ensure Emu68 folder is visible with drawer icon (if created by install_emu68_tools)
if fs_dir "$SYS_ROOT/Emu68"; then
  if ! fs_dir "$SYS_ROOT/Emu68.info"; then
    local _emu_icon_src=""
    if fs_dir "$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info"; then
      _emu_icon_src="$SYS_ROOT/Prefs/Env-Archive/Sys/def_drawer.info"
    elif fs_dir "$SYS_ROOT/Tools.info"; then
      _emu_icon_src="$SYS_ROOT/Tools.info"
    fi
    if [[ -n "$_emu_icon_src" ]]; then
      local _emu_tmp="$STAGE/.emu68-icon-$$.$RANDOM.info"
      if fs_pull_file "$_emu_icon_src" "$_emu_tmp" 2>/dev/null; then
        fs_copy_file "$_emu_tmp" "$SYS_ROOT/Emu68.info" 2>/dev/null || true
      fi; rm -f "$_emu_tmp" 2>/dev/null || true
    fi
    echo "==> Emu68: folder made visible with drawer icon"
  fi
fi

# -----------------------------------------------------------------------------
# 6a.3) Extract Roadshow bsdsocket.library if needed
# -----------------------------------------------------------------------------
# Emu68 emulates 68060 — always use the 68020+ Roadshow variants.
extract_roadshow_bsdsocket "$SYS_ROOT" 1

# IMP3 prefs: PiStorm always has VideoCore RTG
install_imp3_prefs "$SYS_ROOT" 1

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

# -----------------------------------------------------------------------------
# Finalize RunAll LAST
# -----------------------------------------------------------------------------
echo "==> PROGRESS 9/12 Configuring RTG and network"
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"
chmod +x "$TMP_RUNALL"  # Amiga 's' bit: required for Execute in OnetimeRunWB
ensure_img_dir "$SYS_ROOT/S/OneTimeRunWB"
fs_copy_file "$TMP_RUNALL" "$SYS_ROOT/S/OneTimeRunWB/RunAll"

# 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"
chmod +x "$TMP_WB_OTR"  # Amiga 's' bit: required for WBStartup script execution
fs_copy_file_no_makedir "$TMP_WB_OTR" "$SYS_ROOT/WBStartup/OnetimeRunWB"

# Install .info if present in Assets (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"

echo "==> PROGRESS 10/12 Installing IBrowse and WBDock"
# -----------------------------------------------------------------------------
# Install IBrowse/AmiSSL from official website (AFTER hardening to override Ibrowse script)
# js_variant=040fpu: Emu68 emulates 68060 but the 060fpu LibInit() uses an instruction
# Emu68 doesn't handle → OpenLibrary silently returns NULL → "can't open javascript.library".
# 040fpu is binary-compatible with 68060 and avoids this crash.
# TO REVERT: change "040fpu" → "060fpu" in the wrapper call below.
# -----------------------------------------------------------------------------
install_pkg_ibrowse "$SYS_ROOT" "$IBROWSE_DIR" "${IBROWSE_KEY:-}" "040fpu"
# 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)
# -----------------------------------------------------------------------------
install_pkg_wbdock "$SYS_ROOT" "$IBROWSE_DIR"

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

# -----------------------------------------------------------------------------
# NewMeter — apply default tooltypes + PiStorm AK hardware sensor gauges
# -----------------------------------------------------------------------------
configure_newmeter_position "$SYS_ROOT" 5 1

# 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 — uses shared relocate_iprefs_after_loadwb()
# from common.sh (handles WB 3.2 where IPrefs is in WBStartup, not Startup-Sequence,
# case-insensitive Startup-Sequence filename, and CR line ending conversion).
# 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"

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

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

verify_programs_entries "$SYS_ROOT"

# -----------------------------------------------------------------------------
# Sanity check
# -----------------------------------------------------------------------------
echo "==> PROGRESS 11/12 Running sanity checks"
echo "==> Sanity check (key dirs on SYS: and FAT:)"
diag_fs_dir "$FAT_ROOT" || true
diag_fs_dir "$SYS_ROOT" || true
diag_fs_dir "$SYS_ROOT/S" --recursive || true
diag_fs_dir "$SYS_ROOT/WBStartup" --recursive || true
diag_fs_dir "$SYS_ROOT/S/OneTimeRunWB" --recursive || true
run "$HST_IMAGER" rdb info "$RDB" --verbose || true
amigadiskkit_inspect_rdb "$IMG"

# 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 fs_dir "$SYS_ROOT/System/CLI"; then
  echo "==> WARN: System/CLI is a directory (should be a file)"
else
  echo "==> WARN: System/CLI missing (Workbench Shell icon may fail)"
fi
rm -f "$TMP_CHECK_CLI" 2>/dev/null || true

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

# System/Workbench check removed — WB3.2 uses C:LoadWB, not System/Workbench.

patch_runtime_partition_references "$SYS_ROOT"

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