#!/usr/bin/env bash
# Build and deploy OpenWRT images using shell-style device profiles, via source code or the official Image Builder.
# Copyright 2022-25 Bryan C. Roessler
# Apache 2.0 License
# See README and ./profiles for device configuration

# Set default release
: "${DEFAULT_RELEASE:=${RELEASE:="25.12.3"}}"

# @internal
print_help() {
  debug "${FUNCNAME[0]}"

  cat <<-'EOF'
	Build and deploy OpenWRT images using convenient profiles.

	USAGE:
	  openwrtbuilder [OPTION [VALUE]]... -p PROFILE [-p PROFILE]...

	OPTIONS
	  --profile,-p PROFILE
	  --release,-r,--version,-v RELEASE ("snapshot", "24.10.5")
	    Default: From profile or hardcoded RELEASE    
	  --buildroot,-b PATH
	    Default: location of openwrtbuilder script
	  --cpus,-c NUM
	    Default: # of host CPUS minus 1
	  --mode,-m imagebuilder|source
	    Default: imagebuilder
	  --clean clean|targetclean|dirclean|distclean
	    Optional clean step for source mode
	  --ssh-upgrade HOST
	    Examples: root@192.168.1.1, root@router.lan
	  --ssh-backup SSH_PATH
	    Enabled by default for --ssh-upgrade
	  --flash,-f DEVICE
	    Example: /dev/sdX
	  --depends
	    Force dependency installation
	  --yes,-y
	    Assume yes for all questions (non-interactive)
	  --debug,-d
	  --help,-h

	EXAMPLES
	  openwrtbuilder -p r4s -r snapshot
	  openwrtbuilder -p ax6000 -r 23.05.0-rc3 --mode source --debug
	  openwrtbuilder -p rpi4 -r 24.10.0 --flash /dev/sdX
	  openwrtbuilder -p linksys -r snapshot --ssh-upgrade root@192.168.1.1		  
	EOF
}

# @internal
init() {
  debug "${FUNCNAME[0]}"
  declare -g ID RPM_MGR SCRIPT_DIR DL_TOOL

  ((DEBUG)) || echo "To enable debugging output, use --debug or -d"

  # Save the script directory
  # https://stackoverflow.com/a/4774063
  SCRIPT_DIR=$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")

  if [[ -e "/etc/os-release" ]]; then
    source "/etc/os-release"
  else
    echo "/etc/os-release not found"
    echo "Your OS is unsupported"
    print_help
    exit 1
  fi

  debug "Detected host platform: $ID"

  # normalize distro ID
  case "$ID" in
    debian|arch) ;;
    centos|fedora)
      if command -v dnf &>/dev/null; then
        RPM_MGR="dnf"
      elif command -v yum &>/dev/null; then
        RPM_MGR="yum"
      fi
      ;;
    rhel) ID="centos" ;;
    linuxmint|neon|*ubuntu*) ID="ubuntu" ;;
    *suse*) ID="suse" ;;
    raspbian) ID="debian" ;;
    *)
      echo "Autodetecting distro, this may be unreliable"
      for cmd in dnf yum apt pacman; do
        if command -v "$cmd" &>/dev/null; then
          case "$cmd" in
            dnf) ID="fedora"; RPM_MGR="dnf" ;;
            yum) ID="centos"; RPM_MGR="yum" ;;
            apt) ID="ubuntu" ;;
            pacman) ID="arch" ;;
          esac
          break
        fi
      done
      [[ -z $ID ]] && return 1
      ;;
  esac

  debug "Using host platform: $ID"

  # Set distro-specific functions
  case "$ID" in
    fedora|centos) pkg_install(){ sudo "$RPM_MGR" install -y "$@"; } ;;
    debian|ubuntu) pkg_install(){ sudo apt-get install --ignore-missing -y -q0 "$@"; } ;;
    suse) pkg_install(){ sudo zypper --non-interactive -q install --force --no-confirm "$@"; } ;;
    arch) pkg_install(){ sudo pacman -S --noconfirm --needed "$@"; } ;;
  esac

  if command -v axel &>/dev/null; then
    DL_TOOL="axel"
  elif command -v curl &>/dev/null; then
    DL_TOOL="curl"
  else
    echo "Downloading the Image Builder requires axel or curl"
    return 1
  fi
}

# @description Arguments
parse_input() {
  debug "${FUNCNAME[0]}" "$*"
  declare -ga PROFILES
  declare -gi RESET=0 YES=0 DEBUG=0 FORCE_DEPENDS=0 CPUS=0
  declare -g USER_RELEASE SSH_UPGRADE_PATH SSH_BACKUP_PATH FLASH_DEV USER_MODE USER_CLEAN 
  local long_opts='release:,version:,profile:,buildroot:,cpus:,mode:,clean:'
  long_opts+='ssh-upgrade:,ssh-backup:,flash:,reset,depends,yes,debug,help'

  if _input=$(getopt -o +r:v:p:b:m:c:f:ydh -l $long_opts -- "$@"); then
    eval set -- "$_input"
    while true; do
      case "$1" in
        --release|-r|--version|-v) shift; USER_RELEASE="$1" ;;
        --profile|-p) shift; PROFILES+=("$1") ;;
        --buildroot|-b) shift; BUILD_ROOT="$1" ;;
        --mode|-m) shift; USER_MODE="$1" ;;
        --clean) shift; USER_CLEAN="$1" ;;
        --cpus|-c) shift; CPUS="$1" ;;
        --ssh-upgrade) shift; SSH_UPGRADE_PATH="$1" ;;
        --ssh-backup) shift; SSH_BACKUP_PATH="$1" ;;
        --flash|-f) shift; FLASH_DEV="$1" ;;
        --reset) RESET=1 ;;
        --depends) FORCE_DEPENDS=1 ;;
        --yes|-y) YES=1 ;;
        --debug|-d) echo "Debugging on"; DEBUG=1 ;;
        --help|-h) print_help; exit 0 ;;
        --) shift; break ;;
      esac
      shift
    done
  else
    echo "Incorrect options provided"
    print_help; exit 1
  fi
}

# @description Install build dependencies on major distros
# @arg $1 string Build mode ("source" or "imagebuilder")
install_dependencies() {
  debug "${FUNCNAME[0]}"
  local mode="$1"
  local -a pkg_list
  local lock_file

  # Set appropriate lock file based on mode
  if [[ "$mode" == "source" ]]; then
    lock_file="$BUILD_ROOT/.dependencies_source.lock"
  elif [[ "$mode" == "imagebuilder" ]]; then
    lock_file="$BUILD_ROOT/.dependencies_imagebuilder.lock"
  fi

  [[ -f $lock_file ]] && debug "$lock_file lock file exists, skipping dependency install" && return 0

  if [[ "$mode" == "source" ]]; then
    # For building from source code see:
    # https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem
    case "$ID" in
      fedora|centos)
        pkg_list+=(
          bzip2
          clang # for qosify
          diffutils
          gcc
          gcc-c++
          git
          golang
          llvm15-libs # for qosify
          make
          ncurses-devel
          patch
          perl
          perl-Data-Dumper
          perl-File-Compare
          perl-File-Copy
          perl-FindBin
          perl-IPC-Cmd
          perl-JSON-PP
          perl-Thread-Queue
          perl-Time-Piece
          perl-base
          python3
          python3-devel
          python3-pyelftools
          python3-setuptools
          quilt
          rsync
          swig
          tar
          unzip
          wget
          which
        ) ;;
      debian|ubuntu)
        pkg_list+=(
          build-essential
          clang
          file
          flex
          g++
          gawk
          gcc-multilib
          gettext
          git
          golang
          liblzma-dev
          libncurses5-dev
          libssl-dev
          python3-distutils
          quilt
          rsync
          patch
          unzip
          wget
          zlib1g-dev
        ) ;;
      arch)
        pkg_list+=(
          autoconf
          automake
          base-devel
          bash
          binutils
          bison
          bzip2
          clang
          fakeroot
          file
          findutils
          flex
          gawk
          gcc
          gettext
          git
          golang
          grep
          groff
          gzip
          libelf
          libtool
          libxslt
          m4
          make
          ncurses
          net-snmp
          openssl
          patch
          pkgconf
          python
          quilt
          rsync
          sed
          texinfo
          time
          unzip
          util-linux
          wget
          which
          xz
          zlib
        ) ;;
      *) debug "Unsupported OS, skipping dependency install"; return 1 ;;
    esac
  elif [[ "$mode" == "imagebuilder" ]]; then
    # For Imagebuilder
    case "$ID" in
      fedora|centos)
        pkg_list+=(
          @c-development 
          @development-tools
          @development-libs
          perl-FindBin
          zlib-static
          elfutils-libelf-devel
          gawk
          unzip
          file
          wget
          python3
          axel
          perl-IPC-Cmd
          zstd) ;;
      debian|ubuntu)
        pkg_list+=(
          build-essential
          libncurses5-dev
          libncursesw5-dev
          zlib1g-dev
          gawk
          git
          gettext
          libssl-dev
          xsltproc
          wget
          unzip
          python
          axel
          zstd) ;;
      *) debug "Unsupported OS, skipping dependency install"; return 1 ;;
    esac
  fi

  pkg_install "${pkg_list[@]}" && echo "${pkg_list[@]}" > "$lock_file"
}

# @description Normalize release and set worktree reference
# @arg $1 string Raw release input
# @arg $2 string Build mode ("source" or "imagebuilder")
# @returns string Normalized release and reference
normalize_and_ref() {
  local input="$1" mode="$2"
  local rel ref branch tag

  case "$input" in
    snapshot|latest|main|master)
      rel="snapshot"
      ref="main"
      ;;
    v[0-9][0-9].[0-9][0-9].*|[0-9][0-9].[0-9][0-9].*)
      # strip optional leading “v”
      rel="${input#v}"
      if [[ "$mode" == "source" ]]; then
        branch="openwrt-${rel%.*}"
        tag="v$rel"
        if ask_ok "Use branch $branch HEAD (y) or tag $tag (n)?"; then
          ref="$branch"
        else
          ref="$tag"
        fi
      else
        ref="$rel"
      fi
      ;;
    *)
      if [[ "$mode" == "source" ]]; then
        # arbitrary commit-ish allowed
        rel="$input"
        ref="$input"
      else
        echo "Error: invalid release '$input'" >&2
        exit 1
      fi
      ;;
  esac

  printf '%s %s' "$rel" "$ref"
}

# @description Acquires the OpenWRT Image Builder
get_imagebuilder() {
  debug "${FUNCNAME[0]}" "$*"
  local -a url_file_pairs=("$@")

  for ((i=0; i<${#url_file_pairs[@]}; i+=2)); do
    local url="${url_file_pairs[i]}"
    local file="${url_file_pairs[i+1]}"

    # Check if file exists and ask user to remove and redownload
    if [[ -f $file ]] && ! ask_ok "Use existing $file?"; then
      execute rm -f "$file"
    fi

    # Download the file if it doesn't exist
    if [[ ! -f "$file" ]]; then
      echo "Downloading $url to $file using $DL_TOOL"
      execute "$DL_TOOL" "-o" "$file" "$url"
    fi
  done
}

ssh_backup() {
  debug "${FUNCNAME[0]}"
  local date hostname backup_fname
  printf -v date '%(%Y-%m-%d-%H-%M-%S)T'
  hostname=$(ssh -qt "$SSH_BACKUP_PATH" echo -n \$HOSTNAME)
  backup_fname="backup-$hostname-$date.tar.gz"

  [[ -d "$FILES_DIR" ]] || execute mkdir -p "$FILES_DIR"

  # Make backup archive on remote
  if ! execute ssh -t "$SSH_BACKUP_PATH" sysupgrade -b "/tmp/$backup_fname"; then
    echo "SSH backup failed"
    exit 1
  fi

  # Move backup archive locally
  if ! execute rsync -avz --remove-source-files "$SSH_BACKUP_PATH:/tmp/$backup_fname" "$BUILD_DIR/"; then
    echo "Could not copy SSH backup"
    exit 1
  fi

  # Extract backup archive
  if ! execute tar -C "$FILES_DIR" -xzf "$BUILD_DIR/$backup_fname"; then
    echo "Could not extract SSH backup"
    exit 1
  fi

  execute rm "$BUILD_DIR/$backup_fname"
}

make_images() {
  debug "${FUNCNAME[0]}"
  local -a make_opts; ((DEBUG)) && make_opts+=("V=sc")
  local rootfs_partsize
  
  # Image Builder accepts ROOTFS_PARTSIZE, not CONFIG_TARGET_ROOTFS_PARTSIZE.
  # Parse profile KCONFIGS (scalar string) and extract only this value.
  for kconfig in $KCONFIGS; do
    case "$kconfig" in
      CONFIG_TARGET_ROOTFS_PARTSIZE=*) rootfs_partsize="${kconfig#*=}" ;;
    esac
  done

  local -a make_cmd=(make "${make_opts[@]}" image
    BIN_DIR="$BIN_DIR"
    PROFILE="$DEVICE"
    PACKAGES="$PACKAGES"
    FILES="$FILES_DIR"
    --directory="$BUILD_DIR"
    --jobs="$JOBS")

  [[ -n "$rootfs_partsize" ]] && make_cmd+=("ROOTFS_PARTSIZE=$rootfs_partsize")

  # Reuse the existing output
  # TODO Disable for now since it was causing issues
  # if [[ -d "$BIN_DIR" ]]; then
  #   if ask_ok "$BIN_DIR exists. Rebuild?"; then
  #     execute rm -rf "$BIN_DIR"
  #   else
  #     return 0
  #   fi
  # fi

  # Debug manually so we can log output
  debug "${make_cmd[*]}"
  "${make_cmd[@]}" > "$BUILD_DIR/make.log" 2>&1
}

flash_images() {
  debug "${FUNCNAME[0]}"
  local img_gz="$1"
  local dev="$2"
  local img="${img_gz%.gz}"
  local partitions

  if [[ ! -e "$dev" ]]; then
    echo "The device specified by --flash could not be found"
    return 1
  fi

  [[ -f $img_gz ]] || { echo "$img_gz does not exist"; return 1; }

  execute gunzip -qfk "$img_gz"
    
  echo "Unmounting target device $dev partitions"
  partitions=("$dev"?*)
  execute sudo umount "${partitions[@]}"

  if execute sudo dd if="$img" of="$dev" bs=2M conv=fsync; then
    sync
    echo "Image flashed successfully!"
  else
    echo "dd failed!"
    return 1
  fi
}

ssh_upgrade() {
  debug "${FUNCNAME[0]}"
  local img_gz="$1"
  local ssh_path="$2"
  local img_fname="${img_gz##*/}"

  [[ -f $img_gz ]] || { echo "$img_gz is missing, check build output"; return 1; }

  echo "Copying '$img_gz' to $ssh_path/tmp/$img_fname"
  if ! execute scp "$img_gz" "$ssh_path:/tmp/$img_fname"; then
    echo "Could not copy $img_gz to $ssh_path:/tmp/$img_fname"
    return 1
  fi
      
  echo "Executing remote sysupgrade"
  # This may result in weird exit code from closing the ssh connection
  # shellcheck disable=SC2029
  ssh "$ssh_path" "sysupgrade -F /tmp/$img_fname"
  return 0
}

# @description Builds OpenWRT from source code using the the default buildbot as base
from_source() {
  debug "${FUNCNAME[0]}"
  local src_url="https://github.com/openwrt/openwrt.git"
  local seed_file="$BUILD_DIR/.config"
  local worktree_meta="$SRC_DIR/.git/worktrees/source-$REF"
  local pkg kconfig commit description
  local -a make_opts
  local -a kconfigs=(
    "CONFIG_TARGET_${TARGET%%/*}=y"
    "CONFIG_TARGET_${TARGET//\//_}=y"
    "CONFIG_TARGET_PROFILE=DEVICE_$DEVICE"
    "CONFIG_TARGET_${TARGET//\//_}_DEVICE_$DEVICE=y"
    "CONFIG_TARGET_ROOTFS_${FILESYSTEM^^}=y"
    "CONFIG_TARGET_MULTI_PROFILE=n"
    "CONFIG_BUILDBOT=n"
    "CONFIG_ALL_KMODS=n" 
    "CONFIG_ALL_NONSHARED=n"
    "CONFIG_DEVEL=n"
    "CONFIG_COLLECT_KERNEL_DEBUG=n"
    "CONFIG_SDK=n"
    "CONFIG_SDK_LLVM_BPF=n"
    "CONFIG_IB=n"
    "CONFIG_MAKE_TOOLCHAIN=n"
    "CONFIG_TARGET_PER_DEVICE_ROOTFS=n"
  )

  echo "Building from source is under development"

  # Remove all build directories and worktrees if --reset
  if ((RESET)); then
    if [[ -d "$BUILD_DIR" || -d "$worktree_meta" ]]; then
      execute git -C "$SRC_DIR" worktree remove --force --force "$BUILD_DIR"
      [[ -d "$BUILD_DIR" ]] && execute rm -rf "$BUILD_DIR"
      [[ -d "$worktree_meta" ]] && execute rm -rf "$worktree_meta"
    fi
    [[ -d "$BUILD_DIR" ]] && execute rm -rf "$BUILD_DIR"
  fi

  # Fetch or clone source repo (no local merges)
  if [[ -d "$SRC_DIR" ]]; then
    execute git -C "$SRC_DIR" fetch origin --tags --prune
  else
    execute mkdir -p "$SRC_DIR"
    execute git clone "$src_url" "$SRC_DIR"
  fi

  # Reuse worktree if present; otherwise create it (support branches and tags)
  if git -C "$BUILD_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    execute git -C "$BUILD_DIR" fetch origin --tags --prune
    execute git -C "$BUILD_DIR" reset --hard "origin/$REF" || \
      execute git -C "$BUILD_DIR" reset --hard "$REF" || \
      execute git -C "$BUILD_DIR" checkout --detach "$REF"
  else
    execute git -C "$SRC_DIR" worktree prune --verbose
    # Prefer local tag/branch if present, otherwise use remote-tracking branch
    if ! execute git -C "$SRC_DIR" worktree add --detach "$BUILD_DIR" "$REF"; then
      execute git -C "$SRC_DIR" worktree add --detach "$BUILD_DIR" "origin/$REF"
    fi
  fi

  # Add cherrypicks
  for cherrypick in $CHERRYPICKS; do
    url_branch="${cherrypick%:*}"
    commit="${cherrypick##*:}"
    branch=""
    url="$url_branch"
    if [[ "$url_branch" == *"@"* ]]; then
      url="${url_branch%@*}"
      branch="${url_branch#*@}"
    fi

    remote="${url%.git}"
    remote="${remote##*/}"
    remote=${remote//[^A-Za-z0-9._-]/_}
    [[ -z $remote ]] && remote="cherry"

    if ! git -C "$BUILD_DIR" remote | grep -q "^$remote$"; then
      execute git -C "$BUILD_DIR" remote add "$remote" "$url"
    else
      execute git -C "$BUILD_DIR" remote set-url "$remote" "$url"
    fi

    if [[ -n $branch ]]; then
      execute git -C "$BUILD_DIR" fetch "$remote" "$branch"
    else
      execute git -C "$BUILD_DIR" fetch "$remote"
    fi

    # Verify commit exists before attempting cherry-pick
    if ! git -C "$BUILD_DIR" cat-file -e "$commit^{commit}" 2>/dev/null; then
      debug "Commit $commit not found after fetching from $remote; skipping"
      continue
    fi

    execute git -C "$BUILD_DIR" merge-base --is-ancestor "$commit" HEAD ||
      execute git -C "$BUILD_DIR" cherry-pick "$commit"
  done

  # Merge entire branches
  for branch in $BRANCHES; do
    url_branch="$branch"
    branch=""
    url="$url_branch"
    if [[ "$url_branch" == *"@"* ]]; then
      url="${url_branch%@*}"
      branch="${url_branch#*@}"
    fi

    remote="${url%.git}"
    remote="${remote##*/}"
    remote=${remote//[^A-Za-z0-9._-]/_}
    [[ -z $remote ]] && remote="merge"

    if ! git -C "$BUILD_DIR" remote | grep -q "^$remote$"; then
      execute git -C "$BUILD_DIR" remote add "$remote" "$url"
    else
      execute git -C "$BUILD_DIR" remote set-url "$remote" "$url"
    fi

    if [[ -n $branch ]]; then
      execute git -C "$BUILD_DIR" fetch "$remote" "$branch"
      echo "Merging $remote/$branch into $(git -C "$BUILD_DIR" rev-parse --abbrev-ref HEAD)"
      execute git -C "$BUILD_DIR" merge --allow-unrelated-histories -m "Merge $remote/$branch" "$remote/$branch" || \
        { debug "Merge conflict or failed for $remote/$branch"; return 1; }
    else
      debug "Cannot merge: no branch specified in entry '$branch'"
      continue
    fi
  done
  
  # Print commit info
  commit=$(git -C "$BUILD_DIR" rev-parse HEAD)
  description=$(git -C "$BUILD_DIR" describe --always --dirty)
  echo "Current commit hash: $commit"
  echo "Git worktree description: $description"

  ((DEBUG)) && git --no-pager -C "$BUILD_DIR" log -1

  # Enter worktree
  execute pushd "$BUILD_DIR" || return 1
  
  # Begin OpenWRT build process
  ((DEBUG)) && make_opts+=("V=sc")
  
  # Cleanup build environment: heavy clean only when --reset was used earlier
  # make clean # compiled output
  # make targetclean # compiled output, toolchain
  # make dirclean # compiled output, toolchain, build tools
  # make distclean # compiled output, toolchain, build tools, .config, feeds, .ccache
  if [[ -n $CLEAN ]]; then
    execute make "${make_opts[@]}" "-j1" "$CLEAN"
  else
    debug "Skipping cleanup step"
  fi
  
  # Use a custom (faster) mirror
  execute sed -i -E 's;git.openwrt.org/(feed|project);github.com/openwrt;' feeds.conf.default

  # Update package feed
  ./scripts/feeds update -a -f &&
  ./scripts/feeds install -a -f

  # Add custom packages
  for pkg in $PACKAGES; do
    if [[ $pkg == -* ]]; then
      kconfigs+=("CONFIG_PACKAGE_${pkg#-}=n") # remove package
    else
      kconfigs+=("CONFIG_PACKAGE_$pkg=y") # add package
    fi
  done

  # Add profile kconfig options
  # $KCONFIGS is a scalar, use a loop to split and avoid SC2068
  for kconfig in $KCONFIGS; do
    kconfigs+=("$kconfig")
  done

  # Reset and write options to config seed file
  [[ -f $seed_file ]] && execute rm -f "$seed_file"
  for kconfig in "${kconfigs[@]}"; do
    debug "Writing $kconfig to $seed_file"
    echo "$kconfig" >> "$seed_file"
  done

  # Expand seed into full config
  execute make "${make_opts[@]}" "-j1" defconfig

  # Run serial make download for better reliability
  execute make "${make_opts[@]}" "-j1" download

  # (Optional) Disable multicore make world
  # ((DEBUG)) && make_opts+=("-j1") || make_opts+=("-j$JOBS)")
  make_opts+=("-j$JOBS")

  # Make image
  if ! execute ionice -c2 -n7 nice -n19 make "${make_opts[@]}" BIN_DIR="$BIN_DIR" world; then
    echo "Error: make failed"
    return 1
  fi

  execute popd || return 1

  # Symlink output images to root of BIN_DIR (match Image Builder behavior)
  shopt -s nullglob
  for image in "$BIN_DIR/targets/${TARGET}/"*.{img,img.gz,ubi}; do
    execute ln -fs "$image" "$BIN_DIR/${image##*/}"
  done
  shopt -u nullglob

  return 0
}

# @section Helper functions
# @internal
debug() { ((DEBUG)) && echo "Debug: $*"; }
ask_ok() {
  ((YES)) && return
  local r
  read -r -p "$* [y/N]: " r
  r=${r,,}
  [[ "$r" =~ ^(yes|y)$ ]]
}
extract() {
  local archive="$1"
  local out_dir="$2"
  if ! execute tar -axf "$archive" -C "$out_dir" --strip-components 1; then
    echo "Extraction failed"
    return 1
  fi
}
verify() {
  local file_to_check="$1" sum_file="$2"
  local checksum
  command -v sha256sum &>/dev/null || return 1
  [[ -f $sum_file && -f $file_to_check ]] || return 1
  checksum=$(grep "${file_to_check##*/}" "$sum_file" | cut -f1 -d' ')
  echo -n "$checksum $file_to_check" | sha256sum --check --status
}
load() {
  local source_file="$1"
  # shellcheck disable=SC1090
  [[ -f $source_file ]] && source "$source_file"
}
execute() {
  if debug "$*"; then
    "$@"
  else
    "$@" &>/dev/null
  fi
}

# @description The openwrtbuilder main function
# @internal
main() {
  debug "${FUNCNAME[0]}"

  init
  load "$SCRIPT_DIR/profiles"
  parse_input "$@"

  # Fallback to SCRIPT_DIR if BUILD_ROOT has not been set
  declare -g BUILD_ROOT="${BUILD_ROOT:=$SCRIPT_DIR}"
  declare -g FILES_DIR="${FILES_DIR:=$BUILD_ROOT/src/files}"
  declare -g BACKUP_DIR="$SCRIPT_DIR/backups"

  # This could be dangerous
  if [[ $BUILD_ROOT == "/" ]]; then
    echo "Invalid --buildroot"
    exit 1
  fi

  for dir in "$BUILD_ROOT/src" "$BUILD_ROOT/bin"; do
    [[ -d "$dir" ]] || execute mkdir -p "$dir"
  done

  # Allow --reset without a profile
  if ((RESET)) && [[ ${#PROFILES} -lt 1 ]]; then
    for d in "$BUILD_ROOT/src" "$BUILD_ROOT/bin"; do
      ask_ok "Remove $d?" && execute rm -rf "$d"
    done
    exit $?
  fi

  # Remove dependency lock files for --depends
  if ((FORCE_DEPENDS)); then
    [[ -f "$BUILD_ROOT/.dependencies_source.lock" ]] && rm -f "$BUILD_ROOT/.dependencies_source.lock"
    [[ -f "$BUILD_ROOT/.dependencies_imagebuilder.lock" ]] && rm -f "$BUILD_ROOT/.dependencies_imagebuilder.lock"
  fi

  # Run selected profiles
  for PROFILE in "${PROFILES[@]}"; do
    debug "Running profile: $PROFILE"

    if [[ ! ${!PROFILE@a} = A ]]; then
      echo "Profile '$PROFILE' does not exist"
      return 1
    fi

    # Store profile in P_ARR nameref and set global profile vars
    local -n P_ARR="$PROFILE"
    declare -g REPO="${P_ARR[repo]:-}"
    declare -g FILESYSTEM="${P_ARR[filesystem]:="squashfs"}"
    declare -g TARGET="${P_ARR[target]}"
    declare -g DEVICE="${P_ARR[device]}"
    declare -g MODE="${USER_MODE:-${P_ARR[mode]:-imagebuilder}}"
    declare -g CLEAN="${USER_CLEAN:-${P_ARR[clean]:-}}"
    declare -g PACKAGES="${P_ARR[packages]:-}" # scalar
    declare -g CHERRYPICKS="${P_ARR[cherrypicks]:-}" # scalar
    declare -g BRANCHES="${P_ARR[branches]:-}" # scalar
    declare -g KCONFIGS="${P_ARR[kconfigs]:-}" # scalar

    install_dependencies "$MODE"

    # Set number of parallel jobs for make and imagebuilder
    declare -gi JOBS
    if ((CPUS)); then
      JOBS="$CPUS" # user overide (--cpus)
    else
      JOBS=$(nproc || echo 4) # fallback to quad-core if nproc fails
      ((JOBS > 1)) && JOBS=$((JOBS - 1)) # leave one CPU free
    fi

    # Normalize RELEASE and set REF committish
    local raw_release="${USER_RELEASE:=${P_ARR[release]:=$DEFAULT_RELEASE}}"
    declare -g RELEASE REF
    read -r RELEASE REF < <(normalize_and_ref "$raw_release" "$MODE")

    declare -g SRC_DIR="$BUILD_ROOT/src/.openwrt"
    declare -g BUILD_DIR="$BUILD_ROOT/src/$PROFILE/$REF-$MODE"
    declare -g BIN_DIR="$BUILD_ROOT/bin/$PROFILE/$REF-$MODE"

    if [[ "$RELEASE" == "snapshot" ]]; then
      local url_prefix="https://downloads.openwrt.org/snapshots/targets/$TARGET"
      local url_filename="openwrt-imagebuilder-${TARGET//\//-}.Linux-x86_64.tar.zst"
      local img_fname="openwrt-${TARGET//\//-}-$DEVICE-$FILESYSTEM" 
    else
      local url_prefix="https://downloads.openwrt.org/releases/$RELEASE/targets/$TARGET"
      local url_filename="openwrt-imagebuilder-$RELEASE-${TARGET//\//-}.Linux-x86_64.tar.zst"
      local img_fname="openwrt-$RELEASE-${TARGET//\//-}-$DEVICE-$FILESYSTEM"
    fi

    local imagebuilder_url="$url_prefix/$url_filename"
    local imagebuilder_file="$BUILD_DIR/$url_filename"
    local imagebuilder_sha256_url="$url_prefix/sha256sums"
    local imagebuilder_sha256_file="$BUILD_DIR/sha256sums"

    if [[ "$MODE" == "source" ]]; then
      declare -g SYSUPGRADEIMGGZ="$BIN_DIR/targets/$TARGET/$img_fname-sysupgrade.img.gz"
    else
      declare -g SYSUPGRADEIMGGZ="$BUILD_DIR/$img_fname-sysupgrade.img.gz"
    fi

    # Backup existing output directory
    if [[ -d "$BIN_DIR" ]]; then
      local timestamp
      timestamp=$(date +%y%m%d%H%M)
      execute mkdir -p "$BACKUP_DIR/$PROFILE/$REF-$MODE-$timestamp"
      execute rsync -a --delete --exclude 'packages/' "$BIN_DIR/" "$BACKUP_DIR/$PROFILE/$REF-$MODE-$timestamp/"
    fi

    if [[ "$MODE" == "source" ]]; then
      from_source || return $?
    elif [[ "$MODE" == "imagebuilder" ]]; then
      [[ -d $BUILD_DIR ]] || mkdir -p "$BUILD_DIR"
      get_imagebuilder "$imagebuilder_url" "$imagebuilder_file" "$imagebuilder_sha256_url" "$imagebuilder_sha256_file" && 
      verify "$imagebuilder_file" "$imagebuilder_sha256_file" &&
      extract "$imagebuilder_file" "$BUILD_DIR" || return $?

      # Add external repositories for the Image Builder build
      if [[ -n $REPO ]]; then
        if ! grep -q "$REPO" "$BUILD_DIR/repositories.conf"; then
          echo "$REPO" >> "$BUILD_DIR/repositories.conf"
        fi
        sed -i '/option check_signature/d' "$BUILD_DIR/repositories.conf"
      fi
      make_images
      # Verify output image for stock builds (in testing)
      if [[ ! -v P_ARR[packages] || -z ${P_ARR[packages]} ]]; then
        shopt -s nullglob
        local -a outfiles=("$BIN_DIR"/*.img.gz "$BIN_DIR"/*.img)
        shopt -u nullglob
        for outfile in "${outfiles[@]}"; do
          verify "$outfile" "$imagebuilder_sha256_file" || return 1
        done
      fi
      #copyFiles
    fi

    [[ -v SSH_BACKUP_PATH ]] && ssh_backup
    [[ -v SSH_UPGRADE_PATH ]] && ssh_upgrade "$SYSUPGRADEIMGGZ" "$SSH_UPGRADE_PATH"
    [[ -v FLASH_DEV ]] && flash_images "$SYSUPGRADEIMGGZ" "$FLASH_DEV"
  done
}

# Roughly turn debugging on for pre-init
# Reset and reparse in parse_input() with getopt
[[ " $* " =~ ( --debug | -d ) ]] && DEBUG=1

main "$@"
exit
