#!/usr/bin/env bash
# Build and verify Zyryn's self-contained Linux tarball.
#
# The archive deliberately uses an AppDir-style directory instead of AppImage:
# users extract Zyryn/ and run Zyryn/zyryn, with no FUSE mount or package install.
# The Flutter bundle and the LGPL mpv/FFmpeg prefix must already be built on the
# oldest supported Linux baseline (currently Ubuntu 22.04).
set -euo pipefail

readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
readonly EXPECTED_MPV_VERSION="0.36.0"
readonly EXPECTED_MPV_CLIENT_API_VERSION="2.1.0"
readonly EXPECTED_MPV_SOURCE_SHA256="29abc44f8ebee013bb2f9fe14d80b30db19b534c679056e4851ceadf5a5e8bf6"
readonly EXPECTED_FFMPEG_VERSION="6.0"
readonly EXPECTED_FFMPEG_SOURCE_SHA256="57be87c22d9b49c112b6d24bc67d42508660e6b718b3db89c44e47e289137082"

BUNDLE="$ROOT/build/linux/x64/release/bundle"
RUNTIME_PREFIX="${ZYRYN_LINUX_RUNTIME_PREFIX:-$ROOT/build/linux-media-runtime}"
OUTPUT="$ROOT/build/release/Zyryn-linux-x64.tar.gz"
VERIFY_ARCHIVE=""
VERIFY_RUN=""
WORK_DIR=""

fail() {
  echo "ERROR: $*" >&2
  exit 1
}

log() {
  echo "[linux-portable] $*"
}

usage() {
  printf '%s\n' \
    "Usage:" \
    "  scripts/package_linux_portable.sh [--bundle PATH] [--runtime-prefix PATH] [--output PATH]" \
    "  scripts/package_linux_portable.sh --verify-only ARCHIVE" \
    "  scripts/package_linux_portable.sh --verify-run WRAPPER" \
    "" \
    "Defaults:" \
    "  --bundle         build/linux/x64/release/bundle" \
    "  --runtime-prefix \${ZYRYN_LINUX_RUNTIME_PREFIX:-build/linux-media-runtime}" \
    "  --output         build/release/Zyryn-linux-x64.tar.gz"
}

cleanup() {
  if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ]; then
    rm -rf -- "$WORK_DIR"
  fi
}
trap cleanup EXIT INT TERM

while [ "$#" -gt 0 ]; do
  case "$1" in
    --bundle)
      [ "$#" -ge 2 ] || fail "--bundle requires a path."
      BUNDLE="$2"
      shift 2
      ;;
    --runtime-prefix)
      [ "$#" -ge 2 ] || fail "--runtime-prefix requires a path."
      RUNTIME_PREFIX="$2"
      shift 2
      ;;
    --output)
      [ "$#" -ge 2 ] || fail "--output requires a path."
      OUTPUT="$2"
      shift 2
      ;;
    --verify-only)
      [ "$#" -ge 2 ] || fail "--verify-only requires an archive path."
      [ -z "$VERIFY_ARCHIVE" ] || fail "--verify-only may be supplied only once."
      VERIFY_ARCHIVE="$2"
      shift 2
      ;;
    --verify-run)
      [ "$#" -ge 2 ] || fail "--verify-run requires a wrapper path."
      [ -z "$VERIFY_RUN" ] || fail "--verify-run may be supplied only once."
      VERIFY_RUN="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      fail "Unknown argument '$1'. Run with --help for usage."
      ;;
  esac
done

require_command() {
  command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' was not found."
}

sha256_of() {
  sha256sum "$1" | awk '{print $1}'
}

is_elf() {
  [ -f "$1" ] && readelf -h "$1" >/dev/null 2>&1
}

is_dynamic_elf() {
  local elf_type
  is_elf "$1" || return 1
  elf_type="$(LC_ALL=C readelf -h "$1" | awk '/^[[:space:]]*Type:/{print $2; exit}')"
  [ "$elf_type" = "DYN" ] || [ "$elf_type" = "EXEC" ]
}

assert_x86_64_elf() {
  local path="$1"
  LC_ALL=C readelf -h "$path" | grep -Eq \
    'Machine:[[:space:]]*(Advanced Micro Devices X86-64|AMD x86-64)' || \
    fail "Non-x86_64 ELF found in Linux x64 archive: $path"
}

needed_libraries() {
  LC_ALL=C readelf -d "$1" 2>/dev/null | \
    sed -n 's/.*(NEEDED).*Shared library: \[\([^]]*\)\].*/\1/p'
}

# These interfaces belong to the host ABI. Bundling glibc is unsupported, and
# bundling graphics dispatch/driver libraries can make a portable application
# select libraries that do not match the user's actual GPU driver.
is_host_abi_library() {
  case "$1" in
    ld-linux-x86-64.so.2|libc.so.6|libm.so.6|libdl.so.2|libpthread.so.0|\
    librt.so.1|libresolv.so.2|libutil.so.1|libanl.so.1|\
    libstdc++.so.6|libgcc_s.so.1|\
    libGL.so.1|libEGL.so.1|libGLX.so.0|libOpenGL.so.0|\
    libdrm.so.2|libgbm.so.1|libvulkan.so.1|\
    libva.so.2|libva-drm.so.2|libva-wayland.so.2|libva-x11.so.2|libvdpau.so.1|\
    libX11.so.6|libX11-xcb.so.1)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

find_named_library() {
  local name="$1"
  shift
  local directory candidate

  for directory in "$@"; do
    [ -d "$directory" ] || continue
    # A few distro libraries keep private ABI companions in a child directory
    # (for example pulseaudio/libpulsecommon-*), so search below each known,
    # architecture-specific library root as well as its top level.
    candidate="$(find "$directory" -maxdepth 4 \( -type f -o -type l \) \
      -name "$name" -print -quit 2>/dev/null || true)"
    if [ -n "$candidate" ]; then
      readlink -f -- "$candidate"
      return 0
    fi
  done
  return 1
}

discover_runtime_library_dirs() {
  local prefix="$1"
  find "$prefix" -maxdepth 4 -type d \
    \( -name lib -o -name lib64 -o -name x86_64-linux-gnu \) -print 2>/dev/null | \
    LC_ALL=C sort -u
}

resolve_library() {
  local name="$1"
  shift
  local result cache_result
  local search_dirs=("$@")

  result="$(find_named_library "$name" "${search_dirs[@]}" || true)"
  if [ -n "$result" ]; then
    printf '%s\n' "$result"
    return 0
  fi

  if command -v ldconfig >/dev/null 2>&1; then
    cache_result="$(ldconfig -p 2>/dev/null | \
      awk -v wanted="$name" '$1 == wanted && !found {print $NF; found = 1}')"
    if [ -n "$cache_result" ] && [ -f "$cache_result" ]; then
      readlink -f -- "$cache_result"
      return 0
    fi
  fi

  return 1
}

normalize_file_symlinks() {
  local root="$1" link target
  while IFS= read -r -d '' link; do
    target="$(readlink -f -- "$link" || true)"
    [ -n "$target" ] && [ -f "$target" ] || \
      fail "Archive staging contains a broken or non-file symlink: $link"
    rm -- "$link"
    cp -pL -- "$target" "$link"
  done < <(find "$root" -type l -print0)
}

assert_no_forbidden_bundled_abi() {
  local lib_dir="$1" path name
  while IFS= read -r -d '' path; do
    name="$(basename -- "$path")"
    if is_host_abi_library "$name"; then
      fail "Host ABI library must not be bundled: $name"
    fi
  done < <(find "$lib_dir" -maxdepth 1 -type f -print0)
}

record_system_library_notice() {
  local source="$1" soname="$2" package_root="$3"
  local source_real package_line package_record binary_package package_name package_version
  local copyright_file notice_dir manifest

  source_real="$(readlink -f -- "$source")"
  case "$source_real" in
    /lib/*|/usr/lib/*) ;;
    *) return 0 ;;
  esac

  package_line="$(dpkg-query -S "$source_real" 2>/dev/null || true)"
  if [ -z "$package_line" ]; then
    case "$source_real" in
      /usr/lib/*)
        package_line="$(dpkg-query -S "/lib/${source_real#/usr/lib/}" \
          2>/dev/null || true)"
        ;;
    esac
  fi
  if [ -z "$package_line" ]; then
    package_line="$(dpkg-query -S "*/$(basename -- "$source_real")" \
      2>/dev/null || true)"
  fi
  package_record="$(printf '%s\n' "$package_line" | \
    awk -F ': /' 'NR == 1 { print $1; exit }')"
  [ -n "$package_record" ] || \
    fail "Could not identify the Debian package that supplied $source_real."
  binary_package="$package_record"
  package_name="${binary_package%%:*}"
  package_version="$(dpkg-query -W -f='${Version}\n' "$binary_package" 2>/dev/null || true)"
  [ -n "$package_version" ] || \
    fail "Could not identify the installed version of $binary_package."
  copyright_file="/usr/share/doc/$package_name/copyright"
  [ -f "$copyright_file" ] || \
    fail "Debian copyright notice is missing for $binary_package."

  notice_dir="$package_root/licenses/system"
  manifest="$package_root/.system-libraries.unsorted"
  mkdir -p -- "$notice_dir"
  install -m 0644 -- "$copyright_file" \
    "$notice_dir/$package_name-copyright.txt"
  printf '%s\t%s\t%s\t%s\n' \
    "$soname" "$binary_package" "$package_version" "$source_real" >>"$manifest"
}

finalize_system_library_notices() {
  local package_root="$1" unsorted="$package_root/.system-libraries.unsorted"
  local manifest="$package_root/licenses/system/SYSTEM-LIBRARIES.txt"
  local common_source="/usr/share/common-licenses" common_destination source

  [ -s "$unsorted" ] || fail "No bundled system-library provenance was recorded."
  {
    printf '%s\n' \
      'Bundled Ubuntu 22.04 runtime libraries' \
      '' \
      'SONAME | binary package | package version | build-host source path'
    LC_ALL=C sort -u "$unsorted" | tr '\t' '|'
  } >"$manifest"
  rm -f -- "$unsorted"

  [ -d "$common_source" ] || fail "Debian common license texts are missing."
  common_destination="$package_root/licenses/system/common-licenses"
  mkdir -p -- "$common_destination"
  while IFS= read -r -d '' source; do
    install -m 0644 -- "$source" \
      "$common_destination/$(basename -- "$source")"
  done < <(find -L "$common_source" -maxdepth 1 -type f -print0)
  [ -s "$common_destination/LGPL-2.1" ] || \
    fail "Debian LGPL-2.1 common license text was not copied."
}

copy_dependency_closure() {
  local app_dir="$1" runtime_prefix="$2"
  local package_root
  local lib_dir="$app_dir/lib"
  local runtime_real mpv_source mpv_real mpv_pc mpv_pc_version runtime_provenance
  local mpv_source_version ffmpeg_source_version
  local path needed source destination index
  local -a dlopen_runtimes=(libGLESv2.so.2)
  local -a runtime_dirs=()
  local -a system_dirs=(
    /lib/x86_64-linux-gnu
    /usr/lib/x86_64-linux-gnu
    /lib64
    /usr/lib64
    /lib
    /usr/lib
  )
  local -a queue=()
  declare -A processed=()

  package_root="$(dirname -- "$app_dir")"

  [ -d "$runtime_prefix" ] || \
    fail "Custom LGPL runtime prefix not found: $runtime_prefix"
  runtime_real="$(readlink -f -- "$runtime_prefix")"
  mapfile -t runtime_dirs < <(discover_runtime_library_dirs "$runtime_real")
  [ "${#runtime_dirs[@]}" -gt 0 ] || \
    fail "No library directories found below runtime prefix: $runtime_prefix"

  # Never permit the host's default (normally GPL-enabled) libmpv into Zyryn.
  mpv_source="$(find "$runtime_real" -maxdepth 5 \( -type f -o -type l \) \
    -name 'libmpv.so.2' -print -quit 2>/dev/null || true)"
  [ -n "$mpv_source" ] || \
    fail "Pinned libmpv.so.2 was not found below custom runtime prefix $runtime_prefix."
  mpv_real="$(readlink -f -- "$mpv_source")"
  case "$mpv_real" in
    "$runtime_real"/*) ;;
    *) fail "Custom libmpv resolves outside the declared runtime prefix: $mpv_source" ;;
  esac
  assert_x86_64_elf "$mpv_real"
  [ -n "$(LC_ALL=C readelf -d "$mpv_real" | \
    grep -F -m1 'Library soname: [libmpv.so.2]' || true)" ] || \
    fail "Custom libmpv does not declare SONAME libmpv.so.2: $mpv_real"

  mpv_pc="$(find "$runtime_real" -maxdepth 6 -type f -path '*/pkgconfig/mpv.pc' \
    -print -quit 2>/dev/null || true)"
  [ -n "$mpv_pc" ] || fail "Custom runtime prefix is missing mpv.pc provenance."
  mpv_pc_version="$(sed -n 's/^Version:[[:space:]]*//p' "$mpv_pc" | head -1)"
  [ "$mpv_pc_version" = "$EXPECTED_MPV_CLIENT_API_VERSION" ] || \
    fail "Custom libmpv client API '$mpv_pc_version' is not pinned $EXPECTED_MPV_CLIENT_API_VERSION."
  runtime_provenance="$runtime_real/share/zyryn-linux-runtime/BUILD-PROVENANCE.txt"
  mpv_source_version="$runtime_real/share/zyryn-linux-runtime/mpv-source-VERSION"
  ffmpeg_source_version="$runtime_real/share/zyryn-linux-runtime/ffmpeg-source-VERSION"
  [ "$(tr -d '\r\n' <"$mpv_source_version" 2>/dev/null || true)" = \
    "$EXPECTED_MPV_VERSION" ] || \
    fail "Custom runtime mpv source VERSION is not pinned $EXPECTED_MPV_VERSION."
  [ "$(tr -d '\r\n' <"$ffmpeg_source_version" 2>/dev/null || true)" = \
    "$EXPECTED_FFMPEG_VERSION" ] || \
    fail "Custom runtime FFmpeg source VERSION is not pinned $EXPECTED_FFMPEG_VERSION."
  [ -f "$runtime_provenance" ] || fail "Custom runtime build provenance is missing."
  grep -Fqx "mpv source SHA-256: $EXPECTED_MPV_SOURCE_SHA256" \
    "$runtime_provenance" || fail "Custom runtime has the wrong mpv source checksum."
  grep -Fqx 'mpv Meson option: gpl=false' "$runtime_provenance" || \
    fail "Custom runtime does not attest mpv gpl=false."
  grep -Fqx "FFmpeg source SHA-256: $EXPECTED_FFMPEG_SOURCE_SHA256" \
    "$runtime_provenance" || fail "Custom runtime has the wrong FFmpeg source checksum."

  install -m 0755 -- "$mpv_real" "$lib_dir/libmpv.so.2"

  # media_kit/libepoxy opens GLES dynamically, so it is absent from DT_NEEDED
  # and would otherwise escape the recursive ELF closure on minimal desktops.
  for needed in "${dlopen_runtimes[@]}"; do
    source="$(resolve_library "$needed" \
      "${runtime_dirs[@]}" "${system_dirs[@]}" || true)"
    [ -n "$source" ] || \
      fail "Required dynamically loaded runtime was not found: $needed"
    install -m 0755 -- "$source" "$lib_dir/$needed"
    record_system_library_notice "$source" "$needed" "$package_root"
  done
  assert_no_forbidden_bundled_abi "$lib_dir"

  while IFS= read -r -d '' path; do
    if is_dynamic_elf "$path"; then
      queue+=("$path")
    fi
  done < <(find "$app_dir" -type f -print0)

  index=0
  while [ "$index" -lt "${#queue[@]}" ]; do
    path="${queue[$index]}"
    index=$((index + 1))
    [ -z "${processed[$path]+set}" ] || continue
    processed["$path"]=1
    assert_x86_64_elf "$path"

    while IFS= read -r needed; do
      [ -n "$needed" ] || continue
      if is_host_abi_library "$needed"; then
        continue
      fi

      destination="$lib_dir/$needed"
      if [ ! -f "$destination" ]; then
        source="$(resolve_library "$needed" \
          "${runtime_dirs[@]}" "${system_dirs[@]}" || true)"
        [ -n "$source" ] || \
          fail "Could not resolve dependency '$needed' required by $path."
        is_elf "$source" || \
          fail "Resolved dependency is not an ELF library: $needed -> $source"
        assert_x86_64_elf "$source"
        install -m 0755 -- "$source" "$destination"
        case "$(readlink -f -- "$source")" in
          "$runtime_real"/*) ;;
          *) record_system_library_notice "$source" "$needed" "$package_root" ;;
        esac
      fi
      queue+=("$destination")
    done < <(needed_libraries "$path")
  done

  assert_no_forbidden_bundled_abi "$lib_dir"
  finalize_system_library_notices "$package_root"
}

patch_relative_rpaths() {
  local app_dir="$1" path current_rpath

  patchelf --set-rpath '$ORIGIN/lib' "$app_dir/zyryn"
  while IFS= read -r -d '' path; do
    is_dynamic_elf "$path" || continue
    patchelf --set-rpath '$ORIGIN' "$path"
    current_rpath="$(patchelf --print-rpath "$path")"
    [ "$current_rpath" = '$ORIGIN' ] || \
      fail "Could not set relative RPATH on $path."
  done < <(find "$app_dir/lib" -type f -print0)

  [ "$(patchelf --print-rpath "$app_dir/zyryn")" = '$ORIGIN/lib' ] || \
    fail "Could not set relative RPATH on the Flutter runner."
}

read_pubspec_version() {
  awk '/^version:[[:space:]]*/ {sub(/\r$/, "", $2); print $2; exit}' \
    "$ROOT/pubspec.yaml"
}

json_string_value() {
  local key="$1" json_file="$2"
  sed -nE "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p" \
    "$json_file" | head -1
}

verify_ffmpeg_license_configuration() {
  local lib_dir="$1" avcodec="$lib_dir/libavcodec.so.60" configuration
  local required
  local -a expected_ffmpeg_libraries=(
    libavcodec.so.60
    libavformat.so.60
    libavutil.so.58
    libavfilter.so.9
    libswscale.so.7
    libswresample.so.4
  )

  for required in "${expected_ffmpeg_libraries[@]}"; do
    [ -f "$lib_dir/$required" ] || \
      fail "Pinned FFmpeg $EXPECTED_FFMPEG_VERSION library is missing: $required"
  done

  configuration="$(strings "$avcodec" | grep -m1 -- '--disable-gpl' || true)"
  [ -n "$configuration" ] || \
    fail "Bundled FFmpeg does not expose the required --disable-gpl build configuration."
  case "$configuration" in
    *--disable-nonfree*) ;;
    *)
    fail "Bundled FFmpeg was not built with --disable-nonfree."
      ;;
  esac
  if printf '%s\n' "$configuration" | grep -Eq -- '(^|[[:space:]])--enable-(gpl|nonfree)([[:space:]]|$)'; then
    fail "Bundled FFmpeg configuration enables GPL or nonfree components."
  fi
}

verify_dynamic_closure() {
  local app_dir="$1" lib_dir="$app_dir/lib"
  local path needed rpath ldd_output

  while IFS= read -r -d '' path; do
    is_dynamic_elf "$path" || continue
    assert_x86_64_elf "$path"

    if [ "$path" = "$app_dir/zyryn" ]; then
      rpath="$(patchelf --print-rpath "$path")"
      [ "$rpath" = '$ORIGIN/lib' ] || \
        fail "Runner RPATH must be exactly \$ORIGIN/lib, found '$rpath'."
    else
      rpath="$(patchelf --print-rpath "$path")"
      [ "$rpath" = '$ORIGIN' ] || \
        fail "Bundled library RPATH must be exactly \$ORIGIN, found '$rpath' in $path."
    fi
    case "$rpath" in
      /*|*:/?*) fail "Absolute RPATH/RUNPATH found in $path: $rpath" ;;
    esac

    while IFS= read -r needed; do
      [ -n "$needed" ] || continue
      if ! is_host_abi_library "$needed" && [ ! -f "$lib_dir/$needed" ]; then
        fail "Archive dependency closure is incomplete: $path needs $needed."
      fi
    done < <(needed_libraries "$path")

    ldd_output="$(LD_LIBRARY_PATH="$lib_dir" ldd "$path" 2>&1 || true)"
    if [ -n "$(printf '%s\n' "$ldd_output" | grep -F -m1 'not found' || true)" ]; then
      printf '%s\n' "$ldd_output" >&2
      fail "ldd found an unresolved dependency in $path."
    fi
  done < <(
    printf '%s\0' "$app_dir/zyryn"
    find "$lib_dir" -type f -print0
  )

  assert_no_forbidden_bundled_abi "$lib_dir"
  verify_ffmpeg_license_configuration "$lib_dir"
}

verify_internal_resolution() {
  local app_dir="$1" lib_dir="$app_dir/lib"
  local media_plugin ldd_output required resolved gles_ldd_output
  local -a internally_bundled=(libmpv.so.2 libgtk-3.so.0 libgdk-3.so.0 libepoxy.so.0)

  media_plugin="$(find "$lib_dir" -maxdepth 1 -type f \
    -name '*media_kit*video*plugin*.so' -print -quit 2>/dev/null || true)"
  [ -n "$media_plugin" ] || \
    fail "media_kit video plugin was not found in the portable archive."
  [ -f "$lib_dir/libGLESv2.so.2" ] || \
    fail "Dynamically loaded media runtime is missing: libGLESv2.so.2"
  [ -f "$lib_dir/libGLdispatch.so.0" ] || \
    fail "Matching GLVND dispatcher is missing: libGLdispatch.so.0"
  gles_ldd_output="$(LD_LIBRARY_PATH="$lib_dir" ldd "$lib_dir/libGLESv2.so.2" 2>&1 || true)"
  resolved="$(printf '%s\n' "$gles_ldd_output" | awk \
    '$1 == "libGLdispatch.so.0" && $2 == "=>" && !found {print $3; found = 1}')"
  [ -n "$resolved" ] && \
    [ "$(readlink -f -- "$resolved")" = "$(readlink -f -- "$lib_dir/libGLdispatch.so.0")" ] || \
    fail "libGLESv2.so.2 did not resolve the matching bundled GL dispatcher."
  ldd_output="$(LD_LIBRARY_PATH="$lib_dir" ldd "$media_plugin" 2>&1 || true)"

  for required in "${internally_bundled[@]}"; do
    [ -f "$lib_dir/$required" ] || fail "Required bundled runtime is missing: $required"
    resolved="$(printf '%s\n' "$ldd_output" | awk -v wanted="$required" \
      '$1 == wanted && $2 == "=>" && !found {print $3; found = 1}')"
    [ -n "$resolved" ] || \
      fail "$required was not resolved while loading $media_plugin."
    [ "$(readlink -f -- "$resolved")" = "$(readlink -f -- "$lib_dir/$required")" ] || \
      fail "$required resolved outside the portable archive: $resolved"
  done
}

smoke_launch() {
  local package_root="$1" log_file status

  [ "${ZYRYN_LINUX_SKIP_SMOKE:-0}" != "1" ] || {
    log "smoke launch skipped by ZYRYN_LINUX_SKIP_SMOKE=1"
    return 0
  }
  command -v timeout >/dev/null 2>&1 || fail "timeout is required for the launch smoke test."

  mkdir -p "$WORK_DIR/smoke-home" "$WORK_DIR/smoke-config" "$WORK_DIR/smoke-cache"
  log_file="$WORK_DIR/smoke.log"
  set +e
  HOME="$WORK_DIR/smoke-home" \
    XDG_CONFIG_HOME="$WORK_DIR/smoke-config" \
    XDG_CACHE_HOME="$WORK_DIR/smoke-cache" \
    timeout 8s "$package_root/zyryn" >"$log_file" 2>&1
  status=$?
  set -e

  if grep -Eiq \
    'error while loading shared libraries|symbol lookup error|undefined symbol|cannot open shared object file' \
    "$log_file"; then
    sed -n '1,120p' "$log_file" >&2
    fail "Portable launcher failed its loader smoke test."
  fi

  case "$status" in
    0|124)
      return 0
      ;;
    126|127|139)
      sed -n '1,120p' "$log_file" >&2
      fail "Portable launcher exited with loader/crash status $status."
      ;;
  esac

  # A headless packaging machine can reach GTK successfully but have no display
  # server. That is a valid loader smoke; clean WSLg/desktop testing remains the
  # release gate for a visible launch.
  if grep -Eiq 'cannot open display|failed to open display|no display server' "$log_file"; then
    return 0
  fi

  sed -n '1,120p' "$log_file" >&2
  fail "Portable launcher exited unexpectedly during smoke test (status $status)."
}

verify_archive() {
  local archive="$1" verify_dir package_root app_dir lib_dir
  local names_file verbose_file pubspec_version expected_version expected_build
  local version_json archive_version archive_build libapp libmpv avcodec
  local mpv_source_version ffmpeg_source_version
  local libmpv_sha avcodec_sha

  [ -f "$archive" ] || fail "Portable Linux archive not found: $archive"
  [ -s "$archive" ] || fail "Portable Linux archive is empty: $archive"
  gzip -t "$archive" || fail "Portable Linux archive failed gzip integrity verification."

  verify_dir="$(mktemp -d "$WORK_DIR/verify.XXXXXX")"
  names_file="$verify_dir/archive-names.txt"
  verbose_file="$verify_dir/archive-verbose.txt"
  tar -tzf "$archive" >"$names_file"
  tar -tvzf "$archive" >"$verbose_file"

  awk '
    /^\// { bad = 1 }
    /(^|\/)\.\.($|\/)/ { bad = 1 }
    /\\/ { bad = 1 }
    $0 !~ /^Zyryn(\/|$)/ { bad = 1 }
    END { exit bad ? 1 : 0 }
  ' "$names_file" || fail "Archive contains an unsafe path or an entry outside Zyryn/."
  if [ -n "$(LC_ALL=C sort "$names_file" | uniq -d | sed -n '1p')" ]; then
    fail "Archive contains duplicate path entries."
  fi
  if awk '
    substr($1, 1, 1) == "l" || substr($1, 1, 1) == "h" { found = 1 }
    END { exit found ? 0 : 1 }
  ' \
    "$verbose_file"; then
    fail "Portable archive must not contain symlinks or hard links."
  fi

  tar -xzf "$archive" -C "$verify_dir" --no-same-owner
  package_root="$verify_dir/Zyryn"
  app_dir="$package_root/app"
  lib_dir="$app_dir/lib"

  [ -x "$package_root/zyryn" ] || fail "Archive launcher Zyryn/zyryn is not executable."
  [ -x "$app_dir/zyryn" ] || fail "Archive Flutter runner Zyryn/app/zyryn is not executable."
  assert_x86_64_elf "$app_dir/zyryn"
  [ -d "$app_dir/data/flutter_assets" ] || fail "Flutter asset bundle is missing."
  [ -d "$lib_dir" ] || fail "Portable runtime library directory is missing."

  pubspec_version="$(read_pubspec_version)"
  expected_version="${pubspec_version%%+*}"
  expected_build="${pubspec_version#*+}"
  [ "$expected_version" != "$pubspec_version" ] || \
    fail "pubspec.yaml version must include a numeric +build suffix."
  case "$expected_build" in
    ''|*[!0-9]*) fail "pubspec.yaml build number '$expected_build' is not numeric." ;;
  esac

  version_json="$app_dir/data/flutter_assets/version.json"
  [ -f "$version_json" ] || fail "Flutter version.json is missing from the archive."
  archive_version="$(json_string_value version "$version_json")"
  archive_build="$(json_string_value build_number "$version_json")"
  [ "$archive_version" = "$expected_version" ] || \
    fail "Archive version '$archive_version' does not match pubspec '$expected_version'."
  [ "$archive_build" = "$expected_build" ] || \
    fail "Archive build '$archive_build' does not match pubspec '$expected_build'."

  libapp="$lib_dir/libapp.so"
  [ -f "$libapp" ] || fail "Flutter AOT library libapp.so is missing."
  [ -n "$(strings "$libapp" | grep -F -m1 'ZYRYN_DIRECT_PROFILE_V1' || true)" ] || \
    fail "Linux AOT library is missing the direct-distribution profile marker."
  if [ -n "$(strings "$libapp" | grep -E -m1 \
    'ZYRYN_(PLAY_STORE_PROFILE_V1|APP_STORE_PROFILE_V1)' || true)" ]; then
    fail "Linux archive contains a store-distribution profile marker."
  fi

  [ -f "$package_root/README.txt" ] || fail "Portable usage README is missing."
  [ -f "$package_root/BUILD-PROVENANCE.txt" ] || fail "Build provenance is missing."
  [ -f "$package_root/licenses/LGPL-2.1-or-later.txt" ] || \
    fail "LGPL license text is missing."
  [ -f "$package_root/licenses/mpv-0.36.0-Copyright.txt" ] || \
    fail "mpv copyright/relicensing notice is missing."
  [ -f "$package_root/licenses/OPEN_SOURCE_NOTICE.txt" ] || \
    fail "Open-source notice is missing."
  [ -s "$package_root/licenses/system/SYSTEM-LIBRARIES.txt" ] || \
    fail "Bundled system-library provenance is missing."
  [ -n "$(find "$package_root/licenses/system" -maxdepth 1 -type f \
    -name '*-copyright.txt' -print -quit 2>/dev/null || true)" ] || \
    fail "Bundled system-library copyright notices are missing."
  [ -s "$package_root/licenses/system/common-licenses/LGPL-2.1" ] || \
    fail "Bundled Debian common license texts are missing."
  mpv_source_version="$package_root/licenses/mpv-source-VERSION"
  ffmpeg_source_version="$package_root/licenses/ffmpeg-source-VERSION"
  [ "$(tr -d '\r\n' <"$mpv_source_version" 2>/dev/null || true)" = \
    "$EXPECTED_MPV_VERSION" ] || fail "Packaged mpv source VERSION is incorrect."
  [ "$(tr -d '\r\n' <"$ffmpeg_source_version" 2>/dev/null || true)" = \
    "$EXPECTED_FFMPEG_VERSION" ] || fail "Packaged FFmpeg source VERSION is incorrect."
  grep -Fq "mpv version: $EXPECTED_MPV_VERSION" "$package_root/BUILD-PROVENANCE.txt" || \
    fail "Build provenance does not identify pinned mpv $EXPECTED_MPV_VERSION."
  grep -Fq 'mpv Meson option: gpl=false' "$package_root/BUILD-PROVENANCE.txt" || \
    fail "Build provenance does not attest the LGPL mpv configuration."
  grep -Fq "FFmpeg version: $EXPECTED_FFMPEG_VERSION" "$package_root/BUILD-PROVENANCE.txt" || \
    fail "Build provenance does not identify pinned FFmpeg $EXPECTED_FFMPEG_VERSION."

  libmpv="$lib_dir/libmpv.so.2"
  avcodec="$lib_dir/libavcodec.so.60"
  [ -f "$libmpv" ] || fail "Pinned libmpv.so.2 is missing."
  [ -f "$avcodec" ] || fail "Pinned libavcodec.so.60 is missing."
  [ -n "$(LC_ALL=C readelf -d "$libmpv" | \
    grep -F -m1 'Library soname: [libmpv.so.2]' || true)" ] || \
    fail "Bundled libmpv does not declare SONAME libmpv.so.2."
  libmpv_sha="$(sha256_of "$libmpv")"
  avcodec_sha="$(sha256_of "$avcodec")"
  grep -Fqx "Bundled libmpv.so.2 SHA-256: $libmpv_sha" \
    "$package_root/BUILD-PROVENANCE.txt" || \
    fail "Bundled libmpv does not match its build-provenance hash."
  grep -Fqx "Bundled libavcodec.so.60 SHA-256: $avcodec_sha" \
    "$package_root/BUILD-PROVENANCE.txt" || \
    fail "Bundled FFmpeg does not match its build-provenance hash."

  [ -f "$package_root/RUNTIME-MANIFEST.sha256" ] || \
    fail "Runtime checksum manifest is missing."
  (
    cd "$package_root"
    sha256sum --quiet -c RUNTIME-MANIFEST.sha256
  ) || fail "A file in the portable archive does not match its checksum manifest."

  verify_dynamic_closure "$app_dir"
  verify_internal_resolution "$app_dir"
  smoke_launch "$package_root"

  log "verified $(basename -- "$archive")"
  log "  version: $archive_version+$archive_build"
  log "  sha256: $(sha256_of "$archive")"
  log "  bytes: $(stat -c '%s' "$archive")"
}

package_archive() {
  local stage package_root app_dir lib_dir pubspec_version version build_number
  local launcher source_epoch temp_archive libmpv_sha avcodec_sha

  [ -d "$BUNDLE" ] || \
    fail "Flutter Linux bundle not found: $BUNDLE (run flutter build linux --release first)."
  [ -x "$BUNDLE/zyryn" ] || fail "Flutter Linux runner is missing or not executable."
  [ -d "$BUNDLE/data/flutter_assets" ] || fail "Flutter Linux assets are missing."
  [ -d "$BUNDLE/lib" ] || fail "Flutter Linux library directory is missing."
  [ -d "$ROOT/assets/licenses" ] || fail "Repository license assets are missing."
  command -v dpkg-query >/dev/null 2>&1 || \
    fail "dpkg-query is required while packaging on the Ubuntu build baseline."

  pubspec_version="$(read_pubspec_version)"
  version="${pubspec_version%%+*}"
  build_number="${pubspec_version#*+}"
  [ "$version" != "$pubspec_version" ] || \
    fail "pubspec.yaml version must include a +build number."
  case "$build_number" in
    ''|*[!0-9]*) fail "pubspec.yaml build number '$build_number' is not numeric." ;;
  esac

  stage="$(mktemp -d "$WORK_DIR/package.XXXXXX")"
  package_root="$stage/Zyryn"
  app_dir="$package_root/app"
  lib_dir="$app_dir/lib"
  mkdir -p "$app_dir" "$package_root/licenses"
  cp -a -- "$BUNDLE/." "$app_dir/"
  normalize_file_symlinks "$app_dir"

  copy_dependency_closure "$app_dir" "$RUNTIME_PREFIX"
  mkdir -p "$lib_dir/gio/modules"
  patch_relative_rpaths "$app_dir"
  verify_dynamic_closure "$app_dir"
  verify_internal_resolution "$app_dir"

  launcher="$package_root/zyryn"
  printf '%s\n' \
    '#!/usr/bin/env bash' \
    'set -euo pipefail' \
    'ZYRYN_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"' \
    'export LD_LIBRARY_PATH="$ZYRYN_DIR/app/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"' \
    '# Do not load host GIO modules against the bundled Ubuntu 22.04 GLib.' \
    'export GIO_MODULE_DIR="$ZYRYN_DIR/app/lib/gio/modules"' \
    'export GSETTINGS_BACKEND=memory' \
    'if [ -d "$ZYRYN_DIR/app/share/glib-2.0/schemas" ]; then' \
    '  export GSETTINGS_SCHEMA_DIR="$ZYRYN_DIR/app/share/glib-2.0/schemas"' \
    'fi' \
    'exec "$ZYRYN_DIR/app/zyryn" "$@"' \
    >"$launcher"
  chmod 0755 "$launcher"

  install -m 0644 "$ROOT/assets/licenses/LGPL-2.1-or-later.txt" \
    "$package_root/licenses/LGPL-2.1-or-later.txt"
  install -m 0644 "$ROOT/assets/licenses/mpv-0.36.0-Copyright.txt" \
    "$package_root/licenses/mpv-0.36.0-Copyright.txt"
  install -m 0644 "$ROOT/assets/licenses/OPEN_SOURCE_NOTICE.txt" \
    "$package_root/licenses/OPEN_SOURCE_NOTICE.txt"
  install -m 0644 \
    "$RUNTIME_PREFIX/share/zyryn-linux-runtime/mpv-source-VERSION" \
    "$package_root/licenses/mpv-source-VERSION"
  install -m 0644 \
    "$RUNTIME_PREFIX/share/zyryn-linux-runtime/ffmpeg-source-VERSION" \
    "$package_root/licenses/ffmpeg-source-VERSION"

  printf '%s\n' \
    'Zyryn for Linux' \
    '' \
    '1. Extract this archive.' \
    '2. Open the Zyryn folder.' \
    '3. Run ./zyryn (or double-click zyryn in a graphical file manager).' \
    '' \
    'All application and media-playback libraries are included. No separate' \
    'libmpv, FFmpeg, GTK, Flatpak, Snap, or AppImage/FUSE install is required.' \
    'The host still supplies the normal Linux kernel, glibc, display server,' \
    'audio service, graphics driver, fonts, and CA certificate store.' \
    >"$package_root/README.txt"

  libmpv_sha="$(sha256_of "$lib_dir/libmpv.so.2")"
  avcodec_sha="$(sha256_of "$lib_dir/libavcodec.so.60")"
  printf '%s\n' \
    'Zyryn portable Linux runtime provenance' \
    '' \
    "Zyryn version: $version" \
    "Zyryn build: $build_number" \
    'Build baseline: Ubuntu 22.04 x86_64' \
    'Distribution profile: direct' \
    '' \
    "mpv version: $EXPECTED_MPV_VERSION" \
    'mpv source: https://github.com/mpv-player/mpv/archive/refs/tags/v0.36.0.tar.gz' \
    "mpv source SHA-256: $EXPECTED_MPV_SOURCE_SHA256" \
    'mpv Meson option: gpl=false' \
    "Bundled libmpv.so.2 SHA-256: $libmpv_sha" \
    '' \
    "FFmpeg version: $EXPECTED_FFMPEG_VERSION" \
    'FFmpeg source: https://ffmpeg.org/releases/ffmpeg-6.0.tar.xz' \
    "FFmpeg source SHA-256: $EXPECTED_FFMPEG_SOURCE_SHA256" \
    'FFmpeg configure options: --disable-gpl --disable-nonfree --enable-shared --disable-static' \
    "Bundled libavcodec.so.60 SHA-256: $avcodec_sha" \
    '' \
    'The packager validates FFmpeg embedded configuration flags, pins the mpv' \
    'pkg-config version, rejects host/default libmpv, and records binary hashes.' \
    'Corresponding-source details and build scripts: https://zyryn.com/open-source' \
    >"$package_root/BUILD-PROVENANCE.txt"

  (
    cd "$package_root"
    find . -type f ! -name RUNTIME-MANIFEST.sha256 -print0 | \
      LC_ALL=C sort -z | xargs -0 sha256sum >RUNTIME-MANIFEST.sha256
  )

  # A fixed owner/order/time and gzip -n make identical inputs reproducible.
  source_epoch="${SOURCE_DATE_EPOCH:-}"
  if [ -z "$source_epoch" ] && command -v git >/dev/null 2>&1; then
    source_epoch="$(git -C "$ROOT" log -1 --format=%ct 2>/dev/null || true)"
  fi
  source_epoch="${source_epoch:-0}"
  case "$source_epoch" in
    *[!0-9]*) fail "SOURCE_DATE_EPOCH must be a non-negative integer." ;;
  esac

  mkdir -p -- "$(dirname -- "$OUTPUT")"
  temp_archive="$WORK_DIR/Zyryn-linux-x64.tar.gz"
  tar --sort=name --format=gnu --owner=0 --group=0 --numeric-owner \
    --mtime="@$source_epoch" -cf - -C "$stage" Zyryn | gzip -n -9 >"$temp_archive"

  verify_archive "$temp_archive"
  mv -f -- "$temp_archive" "$OUTPUT"
  log "wrote $OUTPUT"

  local run_output
  case "$OUTPUT" in
    *.tar.gz) run_output="${OUTPUT%.tar.gz}.run" ;;
    *) run_output="$OUTPUT.run" ;;
  esac
  bash "$SCRIPT_DIR/package_linux_run.sh" \
    --archive "$OUTPUT" \
    --output "$run_output"
}

for command_name in awk basename cp file find grep gzip install ldd patchelf readelf \
  readlink sed sha256sum sort stat strings tar timeout tr uniq xargs; do
  require_command "$command_name"
done

WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/zyryn-linux-portable.XXXXXX")"

[ -z "$VERIFY_ARCHIVE" ] || [ -z "$VERIFY_RUN" ] || \
  fail "--verify-only and --verify-run cannot be combined."

if [ -n "$VERIFY_RUN" ]; then
  [ -f "$SCRIPT_DIR/package_linux_run.sh" ] || \
    fail "Linux one-file wrapper verifier is missing."
  bash "$SCRIPT_DIR/package_linux_run.sh" --verify-only "$VERIFY_RUN"
elif [ -n "$VERIFY_ARCHIVE" ]; then
  verify_archive "$VERIFY_ARCHIVE"
else
  [ -f "$SCRIPT_DIR/package_linux_run.sh" ] || \
    fail "Linux one-file wrapper packager is missing."
  package_archive
fi
