#!/usr/bin/env bash

if [ -z "${BASH_VERSION:-}" ] || [ "${POSIXLY_CORRECT:-}" = "y" ]; then
  if command -v bash >/dev/null 2>&1; then
    exec bash "$0" "$@"
  fi
  echo "Error: bash is required to run this script" >&2
  exit 1
fi

set -euo pipefail

# Prepares a local runtime directory with qodercli and qoderwake.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P 2>/dev/null)" || SCRIPT_DIR="$(pwd -P)"
REPO_ROOT="${QODERWAKE_REPO_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd -P 2>/dev/null || pwd -P)}"
DEFAULT_QODERCLI_VERSION="1.1.8"
DEFAULT_QODERCLI_BASE_URL="https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/1.1.8"
DEFAULT_QODERCLI_BINARY_NAME="qodercli-wake"
QODERCLI_RELEASE_JSON='{"version":"1.1.8","baseUrl":"https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/1.1.8","archives":{"darwin-arm64":{"file":"qodercli-darwin-arm64.tar.gz","sha256":"b62584d65bcee7a3ff76f8bb7e8ef94c54011ae691c367605c668d6b71ff506e"},"darwin-amd64":{"file":"qodercli-darwin-x64.tar.gz","sha256":"94b393f4acbc6e0480c1222cd956eea62fab822b766cc7a1bc148357352d6ac0"},"linux-amd64":{"file":"qodercli-linux-x64.tar.gz","sha256":"8d0daf68880f1f021de68c5b47909babb69cae36b81bb71380c37d97b0df2199"},"linux-arm64":{"file":"qodercli-linux-arm64.tar.gz","sha256":"ee3d227e31a3926705cb8563d82c682f96fd75f494a26cd1269b05960ae2546f"},"windows-amd64":{"file":"qodercli-windows-x64.zip","sha256":"a8b6acdea3a086e8e7c7579011cfb89840017e7142bbc5f3db242b2a7c0ad18e"}}}'
QODERCLI_RELEASE_CONFIG_FILE="$REPO_ROOT/scripts/qodercli-release.json"
DEFAULT_QODERWAKE_MANIFEST_URL="https://download.qoder.com/qoderwake/channels/manifest.json"
DEFAULT_QODERWAKE_BINARY_NAME="qoderwake"
DEFAULT_QODERWAKE_HOME_DIR=".qoderwake"
DEFAULT_QODERWAKE_PORT="19820"
QODERWAKE_MANIFEST_URL_PLACEHOLDER="__QODERWAKE_MANIFEST_""URL__"
QODERWAKE_BINARY_NAME_PLACEHOLDER="__QODERWAKE_BINARY_""NAME__"
QODERWAKE_HOME_DIR_PLACEHOLDER="__QODERWAKE_HOME_""DIR__"
QODERWAKE_PORT_PLACEHOLDER="__QODERWAKE_""PORT__"
QODERCLI_BINARY_NAME_PLACEHOLDER="__QODERCLI_BINARY_""NAME__"
if [[ "$DEFAULT_QODERWAKE_MANIFEST_URL" == "$QODERWAKE_MANIFEST_URL_PLACEHOLDER" ]]; then
  DEFAULT_QODERWAKE_MANIFEST_URL="https://download.qoder.com/qoderwake/channels/manifest.json"
fi
if [[ "$DEFAULT_QODERWAKE_BINARY_NAME" == "$QODERWAKE_BINARY_NAME_PLACEHOLDER" ]]; then
  DEFAULT_QODERWAKE_BINARY_NAME="qoderwake"
fi
if [[ "$DEFAULT_QODERWAKE_HOME_DIR" == "$QODERWAKE_HOME_DIR_PLACEHOLDER" ]]; then
  DEFAULT_QODERWAKE_HOME_DIR=".qoderwake"
fi
if [[ "$DEFAULT_QODERWAKE_PORT" == "$QODERWAKE_PORT_PLACEHOLDER" ]]; then
  DEFAULT_QODERWAKE_PORT="19820"
fi
if [[ "$DEFAULT_QODERCLI_BINARY_NAME" == "$QODERCLI_BINARY_NAME_PLACEHOLDER" ]]; then
  DEFAULT_QODERCLI_BINARY_NAME="qodercli-wake"
fi
QODERWAKE_MANIFEST_URL="${QODERWAKE_MANIFEST_URL:-$DEFAULT_QODERWAKE_MANIFEST_URL}"
QODERWAKE_BINARY_NAME="${QODERWAKE_BINARY_NAME:-$DEFAULT_QODERWAKE_BINARY_NAME}"
QODERCLI_BINARY_NAME="${QODERCLI_BINARY_NAME:-$DEFAULT_QODERCLI_BINARY_NAME}"
if [[ "$QODERCLI_RELEASE_CONFIG_FILE" == "$REPO_ROOT/scripts/qodercli-release.json" \
  && "$QODERCLI_BINARY_NAME" == "qodercli-cn-wake" \
  && -f "$REPO_ROOT/scripts/qodercli-release-cn.json" ]]; then
  QODERCLI_RELEASE_CONFIG_FILE="$REPO_ROOT/scripts/qodercli-release-cn.json"
fi
MIN_MACOS_VERSION="13.0"

WORK_DIR="${QODERWAKE_WORK_DIR:-${QODERWAKE_HOME:-$HOME/$DEFAULT_QODERWAKE_HOME_DIR}}"
if [[ "$WORK_DIR" != /* ]]; then
  WORK_DIR="$REPO_ROOT/$WORK_DIR"
fi
BIN_DIR="$WORK_DIR/bin"
REQUIRED_QODERCLI_VERSION="$DEFAULT_QODERCLI_VERSION"
AUTO_INSTALL_DEPS="${QODERWAKE_AUTO_INSTALL_DEPS:-true}"
TMP_DIR=""
BUNDLED_QODERCLI_INSTALLED=false

die() {
  echo "Error: $1" >&2
  exit 1
}

log() {
  echo "==> $1"
}

is_truthy() {
  case "${1:-}" in
    1|true|TRUE|True|yes|YES|Yes|on|ON|On)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

normalize_whitespace() {
  printf '%s' "${1:-}" | tr '\r\n' '  ' | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//'
}

normalize_parser_error() {
  local text
  text="$(normalize_whitespace "${1:-}")"
  printf '%s\n' "$text" | sed -E 's/^jq: error \(at [^)]*\): //'
}

truncate_text() {
  local text
  text="$(normalize_whitespace "${1:-}")"
  local limit="${2:-200}"

  if [[ ${#text} -le $limit ]]; then
    printf '%s\n' "$text"
    return 0
  fi

  printf '%s...\n' "${text:0:limit}"
}

join_lines_csv() {
  awk 'BEGIN { first = 1 } NF { if (!first) printf ", "; printf "%s", $0; first = 0 } END { if (!first) printf "\n" }'
}

command_exists() {
  command -v "$1" >/dev/null 2>&1
}

detect_package_manager() {
  if command_exists brew; then
    printf 'brew\n'
    return 0
  fi
  if command_exists apt-get; then
    printf 'apt-get\n'
    return 0
  fi
  if command_exists dnf; then
    printf 'dnf\n'
    return 0
  fi
  if command_exists yum; then
    printf 'yum\n'
    return 0
  fi
  if command_exists apk; then
    printf 'apk\n'
    return 0
  fi
  return 1
}

run_with_optional_sudo() {
  if [[ "$(id -u)" -eq 0 ]]; then
    "$@"
    return $?
  fi

  if command_exists sudo; then
    sudo "$@"
    return $?
  fi

  "$@"
}

dependency_install_package_name() {
  local dependency="$1"
  local package_manager="$2"

  case "$dependency" in
    curl|wget|tar|unzip)
      printf '%s\n' "$dependency"
      ;;
    python3)
      case "$package_manager" in
        brew) printf 'python\n' ;;
        apt-get|dnf|yum|apk) printf 'python3\n' ;;
        *) return 1 ;;
      esac
      ;;
    checksum-tool)
      case "$package_manager" in
        brew) printf 'perl\n' ;;
        apt-get|dnf|yum|apk) printf 'coreutils\n' ;;
        *) return 1 ;;
      esac
      ;;
    *)
      return 1
      ;;
  esac
}

dependency_install_hint() {
  local dependency="$1"
  local package_manager package_name

  package_manager="$(detect_package_manager || true)"
  [[ -n "$package_manager" ]] || return 1

  package_name="$(dependency_install_package_name "$dependency" "$package_manager" 2>/dev/null || true)"
  [[ -n "$package_name" ]] || return 1

  case "$package_manager" in
    brew)
      printf 'brew install %s\n' "$package_name"
      ;;
    apt-get)
      printf 'sudo apt-get update && sudo apt-get install -y %s\n' "$package_name"
      ;;
    dnf)
      printf 'sudo dnf install -y %s\n' "$package_name"
      ;;
    yum)
      printf 'sudo yum install -y %s\n' "$package_name"
      ;;
    apk)
      printf 'sudo apk add --no-cache %s\n' "$package_name"
      ;;
    *)
      return 1
      ;;
  esac
}

attempt_install_dependency() {
  local dependency="$1"
  local package_manager package_name

  if ! is_truthy "$AUTO_INSTALL_DEPS"; then
    return 1
  fi

  package_manager="$(detect_package_manager || true)"
  [[ -n "$package_manager" ]] || return 1

  package_name="$(dependency_install_package_name "$dependency" "$package_manager" 2>/dev/null || true)"
  [[ -n "$package_name" ]] || return 1

  log "Missing required tool for installer: $dependency. Attempting install via $package_manager ($package_name)"
  case "$package_manager" in
    brew)
      brew install "$package_name"
      ;;
    apt-get)
      run_with_optional_sudo apt-get update
      run_with_optional_sudo apt-get install -y "$package_name"
      ;;
    dnf)
      run_with_optional_sudo dnf install -y "$package_name"
      ;;
    yum)
      run_with_optional_sudo yum install -y "$package_name"
      ;;
    apk)
      run_with_optional_sudo apk add --no-cache "$package_name"
      ;;
    *)
      return 1
      ;;
  esac
}

require_dependency() {
  local dependency="$1"
  local command_name="$2"
  local purpose="$3"
  local hint

  if command_exists "$command_name"; then
    return 0
  fi

  if attempt_install_dependency "$dependency" && command_exists "$command_name"; then
    log "Installed required tool '$command_name' for $purpose"
    return 0
  fi

  hint="$(dependency_install_hint "$dependency" 2>/dev/null || true)"
  if [[ -n "$hint" ]]; then
    if ! is_truthy "$AUTO_INSTALL_DEPS"; then
      die "$command_name is required to $purpose. Install it manually with: $hint"
    fi
    die "$command_name is required to $purpose. Tried to auto-install '$dependency' but it is still unavailable. Install it manually with: $hint"
  fi

  if ! is_truthy "$AUTO_INSTALL_DEPS"; then
    die "$command_name is required to $purpose, and no supported package manager was found for manual install guidance"
  fi

  die "$command_name is required to $purpose, and no supported package manager was found for auto-install"
}

usage() {
  cat <<'USAGE'
Usage:
  ./scripts/install.sh [--login-method BROWSER|PAT]
  bash scripts/install.sh [--login-method BROWSER|PAT]
  sh scripts/install.sh [--login-method BROWSER|PAT]

Options:
  --login-method METHOD  login method: BROWSER (default) or PAT

Environment overrides:
  QODERCLI_OSS_URL    qodercli package URL, default auto-detects current platform
  QODERWAKE_OSS_URL   qoderwake package URL
  QODERWAKE_DAEMON_MODE  restart or foreground, default: restart
  QODERWAKE_HOST      daemon host, default: 127.0.0.1
  QODERWAKE_PORT      daemon port, default: build-specific (global 19820, CN 19830)
  QODERWAKE_WORK_DIR  runtime install directory, default: build-specific home
  QODERWAKE_HOME      compatibility alias for QODERWAKE_WORK_DIR when QODERWAKE_WORK_DIR is unset
  QODERWAKE_BINARY_NAME  qoderwake binary name, default: build-specific
  QODERWAKE_AUTO_INSTALL_DEPS  auto-install missing required tools, default: true
  QODERWAKE_POST_INSTALL_PLUGINS  comma-separated plugin names to enable after install
  QODER_PERSONAL_ACCESS_TOKEN or PAT
USAGE
}

cleanup() {
  if [[ -n "${TMP_DIR:-}" && -d "$TMP_DIR" ]]; then
    rm -rf "$TMP_DIR" 2>/dev/null || true
  fi
}

require_https_url() {
  local name="$1"
  local url="$2"

  [[ -n "$url" ]] || die "$name is empty"
  [[ "$url" == https://* ]] || die "$name must use HTTPS"
}

require_arg() {
  local flag="$1"
  local value="${2:-}"

  [[ -n "$value" ]] || die "$flag requires a value"
}

resolve_pat() {
  if [[ -n "${QODER_PERSONAL_ACCESS_TOKEN:-}" ]]; then
    printf '%s\n' "$QODER_PERSONAL_ACCESS_TOKEN"
    return 0
  fi

  if [[ -n "${PAT:-}" ]]; then
    printf '%s\n' "$PAT"
    return 0
  fi

  die "QODER_PERSONAL_ACCESS_TOKEN or PAT is required"
}

filename_from_url() {
  local url="$1"
  local path="${url%%\?*}"
  local name="${path##*/}"

  [[ -n "$name" ]] || name="download.bin"
  printf '%s\n' "$name"
}

download() {
  local url="$1"
  local output="$2"
  local label="$3"
  local attempt=1

  if ! command_exists curl && ! command_exists wget; then
    attempt_install_dependency curl || attempt_install_dependency wget || true
  fi

  while [[ $attempt -le 3 ]]; do
    if [[ $attempt -gt 1 ]]; then
      echo "Retry $((attempt - 1))/2 for $label..." >&2
      sleep 2
    fi

    if command_exists curl; then
      if curl -fsSL --retry 2 --connect-timeout 30 --max-time 300 \
        -H "User-Agent: install/curl-bash" \
        "$url" -o "$output"; then
        return 0
      fi
    elif command_exists wget; then
      if wget -q --tries=2 --connect-timeout=30 --read-timeout=300 \
        --user-agent="install/curl-bash" \
        "$url" -O "$output"; then
        return 0
      fi
    else
      local hint
      hint="$(dependency_install_hint curl 2>/dev/null || dependency_install_hint wget 2>/dev/null || true)"
      if [[ -n "$hint" ]]; then
        die "Neither curl nor wget is available for downloading $label. Install one with: $hint"
      fi
      die "Neither curl nor wget is available for downloading $label"
    fi

    rm -f "$output" 2>/dev/null || true
    attempt=$((attempt + 1))
  done

  return 1
}

compute_sha256() {
  local file="$1"

  if ! command_exists shasum && ! command_exists sha256sum; then
    attempt_install_dependency checksum-tool || true
  fi

  if command_exists shasum; then
    shasum -a 256 "$file" | awk '{print $1}'
  elif command_exists sha256sum; then
    sha256sum "$file" | awk '{print $1}'
  else
    local hint
    hint="$(dependency_install_hint checksum-tool 2>/dev/null || true)"
    if [[ -n "$hint" ]]; then
      die "shasum or sha256sum is required to verify package checksums. Install it with: $hint"
    fi
    die "shasum or sha256sum is required to verify package checksums"
  fi
}

verify_sha256() {
  local file="$1"
  local expected="$2"
  local label="$3"
  local actual

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

  log "Verifying $label checksum"
  actual="$(compute_sha256 "$file")"
  if [[ "$actual" != "$expected" ]]; then
    die "$label checksum mismatch (expected: $expected, actual: $actual)"
  fi
}

find_binary() {
  local dir="$1"
  local binary_name="$2"
  local candidate

  while IFS= read -r candidate; do
    printf '%s\n' "$candidate"
    return 0
  done < <(find "$dir" -type f -name "$binary_name" -print)

  if [[ "$binary_name" == "qodercli" ]]; then
    while IFS= read -r candidate; do
      printf '%s\n' "$candidate"
      return 0
    done < <(find "$dir" -type f \( -name "qoderclicn" -o -name "qoderclicn.exe" \) -print)
  fi

  return 1
}

extract_archive() {
  local archive="$1"
  local filename="$2"
  local output_dir="$3"

  mkdir -p "$output_dir"

  case "$filename" in
    *.tar.gz|*.tgz)
      require_dependency tar tar "extract $filename"
      tar -xzf "$archive" -C "$output_dir" || die "Failed to extract $filename"
      ;;
    *.zip)
      require_dependency unzip unzip "extract $filename"
      unzip -q "$archive" -d "$output_dir" || die "Failed to extract $filename"
      ;;
    *.tar|*.gz|*.bz2|*.xz|*.7z)
      die "Unsupported archive format: $filename"
      ;;
    *)
      return 1
      ;;
  esac
}

install_binary() {
  local source_path="$1"
  local target_path="$2"
  local target_dir
  target_dir="$(dirname "$target_path")"

  [[ -f "$source_path" ]] || die "Source binary not found: $source_path"
  [[ ! -d "$target_path" ]] || die "Target path is a directory: $target_path"

  mkdir -p "$target_dir"
  rm -f "$target_path"
  cp "$source_path" "$target_path" || die "Failed to install $target_path"
  chmod +x "$target_path" || die "Failed to mark executable: $target_path"
}

install_resources() {
  local extract_dir="$1"
  local target_dir="$2"

  # Copy resources directory if it exists in the archive
  if [[ -d "$extract_dir/resources" ]]; then
    log "Installing resources directory"
    rm -rf "$target_dir/resources" 2>/dev/null || true
    cp -R "$extract_dir/resources" "$target_dir/" || die "Failed to copy resources directory"
  fi
}

download_and_install() {
  local url="$1"
  local label="$2"
  local binary_name="$3"
  local target_path="$4"
  local url_var_name="$5"
  local expected_sha256="${6:-}"
  local filename archive extract_dir source_path target_dir

  filename="$(filename_from_url "$url")"
  archive="$TMP_DIR/$label-$filename"
  extract_dir="$TMP_DIR/$label-extract"
  target_dir="$(dirname "$target_path")"

  log "Downloading $label package: $filename"
  download "$url" "$archive" "$label" || die "Failed to download $label from $url_var_name"
  verify_sha256 "$archive" "$expected_sha256" "$label"

  if extract_archive "$archive" "$filename" "$extract_dir"; then
    source_path="$(find_binary "$extract_dir" "$binary_name")" || die "$binary_name not found in $label package"
    # Install resources directory if present (for qoderwake)
    install_resources "$extract_dir" "$target_dir"
    if [[ "$label" == "qoderwake" && -f "$extract_dir/qodercli/$QODERCLI_BINARY_NAME" ]]; then
      install_binary "$extract_dir/qodercli/$QODERCLI_BINARY_NAME" "./qodercli/$QODERCLI_BINARY_NAME"
      BUNDLED_QODERCLI_INSTALLED=true
      log "Installed bound $QODERCLI_BINARY_NAME to ./qodercli/$QODERCLI_BINARY_NAME"
    fi
  else
    source_path="$archive"
  fi

  install_binary "$source_path" "$target_path"
  log "Installed $label to $target_path"
}

run_step() {
  local status

  log "Running: $*"
  if "$@"; then
    return 0
  else
    status=$?
  fi

  print_command_failure_context "$status" "$@"
  die "Command failed ($status): $*"
}

run_qoder_auth_step() {
  local token="$1"
  local status
  shift

  log "Running: QODER_PERSONAL_ACCESS_TOKEN=*** QODER_ENV=${QODER_ENV:-prod} $*"
  if env QODER_PERSONAL_ACCESS_TOKEN="$token" QODER_ENV="${QODER_ENV:-prod}" "$@"; then
    return 0
  else
    status=$?
  fi

  print_command_failure_context "$status" "$@"
  die "Command failed ($status): $*"
}

ensure_http_client() {
  if command_exists curl || command_exists wget; then
    return 0
  fi

  attempt_install_dependency curl || attempt_install_dependency wget || true
  if command_exists curl || command_exists wget; then
    return 0
  fi

  local hint
  hint="$(dependency_install_hint curl 2>/dev/null || dependency_install_hint wget 2>/dev/null || true)"
  if [[ -n "$hint" ]]; then
    die "curl or wget is required to perform HTTP requests during installation. Install one with: $hint"
  fi

  die "curl or wget is required to perform HTTP requests during installation"
}

fetch_text_without_proxy() {
  local url="$1"
  ensure_http_client

  if command_exists curl; then
    env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u http_proxy -u https_proxy -u all_proxy \
      curl -fsSL --retry 2 --connect-timeout 15 --max-time 30 "$url"
    return $?
  fi

  env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u http_proxy -u https_proxy -u all_proxy \
    wget -q --tries=2 --connect-timeout=15 --read-timeout=30 --user-agent="install/curl-bash" -O - "$url"
}

probe_url_without_proxy() {
  local url="$1"
  ensure_http_client

  if command_exists curl; then
    env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u http_proxy -u https_proxy -u all_proxy \
      curl -fsS --max-time 3 "$url" >/dev/null 2>&1
    return $?
  fi

  env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u http_proxy -u https_proxy -u all_proxy \
    wget -q --spider --timeout=3 --tries=1 "$url" >/dev/null 2>&1
}

detect_os() {
  local os
  os="$(uname -s | tr '[:upper:]' '[:lower:]')"
  case "$os" in
    darwin*)  printf 'darwin' ;;
    linux*)   printf 'linux' ;;
    *mingw*|*cygwin*|*msys*) printf 'windows' ;;
    *)        die "Unsupported OS: $os" ;;
  esac
}

detect_arch() {
  local os arch
  os="${1:-$(detect_os)}"

  if [[ "$os" == "darwin" ]] && [[ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" == "1" ]]; then
    printf 'arm64'
    return 0
  fi

  arch="$(uname -m)"
  case "$arch" in
    x86_64|amd64)   printf 'amd64' ;;
    aarch64|arm64)  printf 'arm64' ;;
    *)              die "Unsupported architecture: $arch" ;;
  esac
}

darwin_x64_lacks_bun_cpu_features() {
  local leaf7_features
  leaf7_features="$(sysctl -n machdep.cpu.leaf7_features 2>/dev/null || true)"

  if [[ " $leaf7_features " == *" AVX2 "* && " $leaf7_features " == *" BMI2 "* ]]; then
    return 1
  fi

  return 0
}

warn_darwin_x64_cpu_compatibility() {
  local os="$1"
  local arch="$2"

  if [[ "$os" == "darwin" && "$arch" == "amd64" ]] && darwin_x64_lacks_bun_cpu_features; then
    log "Warning: this Intel Mac may be unsupported by Bun-based QoderWake because AVX2/BMI2 CPU features are unavailable."
    log "Installation will continue, but qoderwake may exit with illegal instruction on this machine."
  fi
}

version_at_least() {
  local version="$1"
  local minimum="$2"
  local IFS=.
  local -a version_parts minimum_parts
  local i version_part minimum_part

  read -r -a version_parts <<< "$version"
  read -r -a minimum_parts <<< "$minimum"

  for i in 0 1 2; do
    version_part="${version_parts[$i]:-0}"
    minimum_part="${minimum_parts[$i]:-0}"
    [[ "$version_part" =~ ^[0-9]+$ ]] || version_part=0
    [[ "$minimum_part" =~ ^[0-9]+$ ]] || minimum_part=0

    if (( 10#$version_part > 10#$minimum_part )); then
      return 0
    fi
    if (( 10#$version_part < 10#$minimum_part )); then
      return 1
    fi
  done

  return 0
}

assert_supported_platform() {
  local os arch macos_version
  os="$(detect_os)"
  arch="$(detect_arch "$os")"

  if [[ "$os" != "darwin" ]]; then
    return 0
  fi

  macos_version="$(sw_vers -productVersion 2>/dev/null || true)"
  [[ -n "$macos_version" ]] || die "Cannot determine macOS version. QoderWake requires macOS ${MIN_MACOS_VERSION} or later."

  if ! version_at_least "$macos_version" "$MIN_MACOS_VERSION"; then
    die "QoderWake requires macOS ${MIN_MACOS_VERSION} or later (detected macOS ${macos_version}, ${arch}). Upgrade macOS before installing."
  fi
}

print_command_failure_context() {
  local status="$1"
  shift

  if [[ "$status" -ne 132 ]]; then
    return 0
  fi

  local uname_s uname_m macos_version os_lower
  uname_s="$(uname -s 2>/dev/null || true)"
  uname_m="$(uname -m 2>/dev/null || true)"
  os_lower="$(printf '%s' "$uname_s" | tr '[:upper:]' '[:lower:]')"

  log "Command crashed with SIGILL (illegal instruction): $*"
  if [[ "$os_lower" == darwin* ]]; then
    macos_version="$(sw_vers -productVersion 2>/dev/null || true)"
    log "Detected platform: macOS ${macos_version:-unknown} / ${uname_m:-unknown}"
    if [[ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" == "1" && "$uname_m" == "x86_64" ]]; then
      log "This shell appears to be running under Rosetta. QoderWake should install the Apple Silicon arm64 package on this machine."
    elif [[ -n "$macos_version" ]] && ! version_at_least "$macos_version" "$MIN_MACOS_VERSION"; then
      log "QoderWake macOS packages require macOS ${MIN_MACOS_VERSION} or later."
    else
      log "This usually means the installed binary does not match the current machine or the package is damaged."
    fi
  fi
}

validate_installed_binary() {
  local binary="$1"
  local label="$2"
  local output status first_line

  if output="$("$binary" --version 2>&1)"; then
    first_line="$(printf '%s\n' "$output" | head -1)"
    log "Verified $label: ${first_line:-version command succeeded}"
    return 0
  else
    status=$?
  fi

  print_command_failure_context "$status" "$binary" --version
  if [[ -n "${output:-}" ]]; then
    printf '%s\n' "$output" >&2
  fi
  die "Installed $label cannot run ($status): $binary --version"
}

check_qodercli_version() {
  local bin="$WORK_DIR/qodercli/$QODERCLI_BINARY_NAME"
  if [[ ! -x "$bin" ]]; then
    return 1
  fi

  local os arch
  os="$(detect_os)"
  arch="$(detect_arch "$os")"
  if ! installed_binary_matches_arch "$bin" "$os" "$arch"; then
    return 1
  fi

  local version_output
  version_output="$("$bin" --version 2>/dev/null)" || return 1

  local installed_version
  installed_version="$(printf '%s' "$version_output" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" || return 1

  if [[ "$installed_version" == "$REQUIRED_QODERCLI_VERSION" ]]; then
    return 0
  fi
  return 1
}

installed_binary_matches_arch() {
  local bin="$1"
  local os="$2"
  local arch="$3"
  local description

  if [[ "$os" != "darwin" ]]; then
    return 0
  fi

  description="$(file "$bin" 2>/dev/null || true)"
  case "$arch" in
    arm64) [[ "$description" == *"arm64"* ]] ;;
    amd64) [[ "$description" == *"x86_64"* || "$description" == *"amd64"* ]] ;;
    *) return 1 ;;
  esac
}

# Find a directory already in PATH where we can place symlinks.
# Prefers ~/.local/bin (user-writable, standard on modern distros),
# then /usr/local/bin. Returns empty string if none found.
resolve_path_link_dir() {
  for candidate in "$HOME/.local/bin" "/usr/local/bin"; do
    if [[ -d "$candidate" ]] && [[ -w "$candidate" ]]; then
      case ":$PATH:" in
        *":$candidate:"*)
          printf '%s' "$candidate"
          return 0
          ;;
      esac
    fi
  done
  return 1
}

link_to_bin() {
  local source_path="$1"
  local link_name
  link_name="$(basename "$source_path")"

  mkdir -p "$BIN_DIR"
  rm -f "$BIN_DIR/$link_name"
  ln -s "$source_path" "$BIN_DIR/$link_name" || die "Failed to create symlink $BIN_DIR/$link_name"
}

link_to_path_dir() {
  local source_path="$1"
  local target_dir="$2"
  local link_name
  link_name="$(basename "$source_path")"

  rm -f "$target_dir/$link_name"
  ln -s "$source_path" "$target_dir/$link_name" || die "Failed to create symlink $target_dir/$link_name"
}

ensure_in_path() {
  case ":$PATH:" in
    *":$BIN_DIR:"*) return 0 ;;
  esac
  export PATH="$BIN_DIR:$PATH"
  log "Added $BIN_DIR to PATH"

  local profile=""
  for p in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
    if [[ -f "$p" ]]; then
      profile="$p"
      break
    fi
  done
  if [[ -n "$profile" ]] && ! grep -qF "$BIN_DIR" "$profile" 2>/dev/null; then
    if [[ ! -w "$profile" ]]; then
      log "Warning: skipping PATH entry in $profile because it is not writable"
      return 0
    fi
    if printf '\nexport PATH="%s:$PATH"\n' "$BIN_DIR" >> "$profile"; then
      log "Added PATH entry to $profile (restart shell or source it to take effect)"
    else
      log "Warning: failed to update PATH entry in $profile; continuing with $BIN_DIR in the current process PATH"
    fi
  fi
}

parse_manifest_with_jq() {
  local manifest="$1"
  local target_os="$2"
  local target_arch="$3"
  local output

  output="$(printf '%s' "$manifest" | jq -r --arg os "$target_os" --arg arch "$target_arch" '
    if (.files | type) != "array" then
      error("Invalid qoderwake manifest: missing files array")
    else
      (first(.files[] | select(.os == $os and .arch == $arch)) // null) as $entry
      | if $entry == null then
          ""
        else
          [($entry.url // ""), ($entry.sha256 // "")] | @tsv
        end
    end
  ')" || return 2

  [[ -n "$output" ]] || return 1
  printf '%s\n' "$output"
}

parse_manifest_with_python3() {
  local manifest="$1"
  local target_os="$2"
  local target_arch="$3"

  python3 -c '
import json
import sys

target_os = sys.argv[1]
target_arch = sys.argv[2]

try:
    manifest = json.load(sys.stdin)
except Exception as exc:
    sys.stderr.write(f"Invalid qoderwake manifest: {exc}\n")
    sys.exit(2)

files = manifest.get("files") if isinstance(manifest, dict) else None
if not isinstance(files, list):
    sys.stderr.write("Invalid qoderwake manifest: missing files array\n")
    sys.exit(2)

for entry in files:
    if not isinstance(entry, dict):
        continue
    if entry.get("os") == target_os and entry.get("arch") == target_arch:
        url = entry.get("url") or ""
        sha256 = entry.get("sha256") or ""
        sys.stdout.write(f"{url}\t{sha256}\n")
        sys.exit(0)

sys.exit(1)
' "$target_os" "$target_arch" <<<"$manifest"
}

parse_manifest_with_node() {
  local manifest="$1"
  local target_os="$2"
  local target_arch="$3"

  node -e '
const fs = require("fs");
const targetOs = process.argv[1];
const targetArch = process.argv[2];

let manifest;
try {
  manifest = JSON.parse(fs.readFileSync(0, "utf8"));
} catch (error) {
  process.stderr.write(`Invalid qoderwake manifest: ${error.message}\n`);
  process.exit(2);
}

const files = manifest && manifest.files;
if (!Array.isArray(files)) {
  process.stderr.write("Invalid qoderwake manifest: missing files array\n");
  process.exit(2);
}

const entry = files.find((file) => file && file.os === targetOs && file.arch === targetArch);
if (!entry) {
  process.exit(1);
}

process.stdout.write(`${entry.url || ""}\t${entry.sha256 || ""}\n`);
' "$target_os" "$target_arch" <<<"$manifest"
}

extract_manifest_string_field() {
  local entry="$1"
  local field="$2"

  printf '%s\n' "$entry" | sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p"
}

extract_manifest_files_payload() {
  local compact_manifest="$1"
  local after_files files_payload

  after_files="${compact_manifest#*\"files\"}"
  if [[ "$after_files" == "$compact_manifest" ]]; then
    return 1
  fi

  after_files="${after_files#*\[}"
  files_payload="${after_files%%\],\"installers\"*}"
  if [[ "$files_payload" == "$after_files" ]]; then
    files_payload="${after_files%%\],\"nativeApp\"*}"
  fi
  if [[ "$files_payload" == "$after_files" ]]; then
    files_payload="${after_files%%\],\"qodercli\"*}"
  fi
  if [[ "$files_payload" == "$after_files" ]]; then
    files_payload="$(printf '%s\n' "$after_files" | sed 's/\]}$//')"
  fi

  [[ "$files_payload" != "$after_files" && -n "$files_payload" ]] || return 1
  printf '%s\n' "$files_payload"
}

parse_manifest_with_shell() {
  local manifest="$1"
  local target_os="$2"
  local target_arch="$3"
  local compact_manifest files_payload entry entry_os entry_arch url sha256

  # Portable fallback for clean macOS/Linux hosts that do not have jq, python3, or node.
  # It targets the release.sh-generated manifest shape and reads fields by name, so
  # files[] object field order does not matter.
  compact_manifest="$(printf '%s' "$manifest" | tr -d '\n\r\t ')"
  files_payload="$(extract_manifest_files_payload "$compact_manifest")"
  if [[ -z "$files_payload" ]]; then
    printf '%s\n' "Invalid qoderwake manifest: missing files array" >&2
    return 2
  fi

  while IFS= read -r entry; do
    entry_os="$(extract_manifest_string_field "$entry" "os")"
    entry_arch="$(extract_manifest_string_field "$entry" "arch")"
    if [[ "$entry_os" == "$target_os" && "$entry_arch" == "$target_arch" ]]; then
      url="$(extract_manifest_string_field "$entry" "url")"
      sha256="$(extract_manifest_string_field "$entry" "sha256")"
      printf '%s\t%s\n' "$url" "$sha256"
      return 0
    fi
  done < <(printf '%s\n' "$files_payload" | sed 's/},[[:space:]]*{/}\
{/g')

  return 1
}

select_manifest_file_entry() {
  local manifest="$1"
  local target_os="$2"
  local target_arch="$3"

  if command_exists jq; then
    parse_manifest_with_jq "$manifest" "$target_os" "$target_arch"
    return $?
  fi

  if command_exists python3; then
    parse_manifest_with_python3 "$manifest" "$target_os" "$target_arch"
    return $?
  fi

  if command_exists node; then
    parse_manifest_with_node "$manifest" "$target_os" "$target_arch"
    return $?
  fi

  parse_manifest_with_shell "$manifest" "$target_os" "$target_arch"
}

capture_command_result() {
  local command_name="$1"
  shift

  local stdout_file stderr_file status
  stdout_file="$(mktemp "${TMPDIR:-/tmp}/qoderwake-install.stdout.XXXXXX")" || die "Failed to create temp file for installer output"
  stderr_file="$(mktemp "${TMPDIR:-/tmp}/qoderwake-install.stderr.XXXXXX")" || {
    rm -f "$stdout_file"
    die "Failed to create temp file for installer diagnostics"
  }

  "$command_name" "$@" >"$stdout_file" 2>"$stderr_file"
  status=$?
  CAPTURED_STDOUT="$(cat "$stdout_file" 2>/dev/null || true)"
  CAPTURED_STDERR="$(normalize_parser_error "$(cat "$stderr_file" 2>/dev/null || true)")"
  rm -f "$stdout_file" "$stderr_file"
  return $status
}

resolve_json_parser() {
  if command_exists jq; then
    printf 'jq\n'
    return 0
  fi
  if command_exists python3; then
    printf 'python3\n'
    return 0
  fi
  if command_exists node; then
    printf 'node\n'
    return 0
  fi
  printf 'shell\n'
}

list_manifest_platforms() {
  local manifest="$1"
  local compact_manifest files_payload entry entry_os entry_arch

  compact_manifest="$(printf '%s' "$manifest" | tr -d '\n\r\t ')"
  files_payload="$(extract_manifest_files_payload "$compact_manifest")"
  [[ -n "$files_payload" ]] || return 1

  while IFS= read -r entry; do
    entry_os="$(extract_manifest_string_field "$entry" "os")"
    entry_arch="$(extract_manifest_string_field "$entry" "arch")"
    if [[ -n "$entry_os" && -n "$entry_arch" ]]; then
      printf '%s/%s\n' "$entry_os" "$entry_arch"
    fi
  done < <(printf '%s\n' "$files_payload" | sed 's/},[[:space:]]*{/}\
{/g') | join_lines_csv
}

resolve_qoderwake_entry_from_manifest() {
  local manifest="$1"
  local os="$2"
  local arch="$3"
  local entry url sha256 parser_name parse_status available_platforms

  parser_name="$(resolve_json_parser)"
  if capture_command_result "parse_manifest_with_${parser_name}" "$manifest" "$os" "$arch"; then
    entry="$CAPTURED_STDOUT"
  else
    parse_status=$?
    case "$parse_status" in
      1)
        available_platforms="$(list_manifest_platforms "$manifest" 2>/dev/null || true)"
        if [[ -n "$available_platforms" ]]; then
          die "No qoderwake package found for ${os}/${arch} in manifest. Available platforms: $available_platforms"
        fi
        die "No qoderwake package found for ${os}/${arch} in manifest"
        ;;
      2)
        if [[ -n "${CAPTURED_STDERR:-}" ]]; then
          die "$CAPTURED_STDERR"
        fi
        die "Failed to parse qoderwake manifest with $parser_name. Manifest preview: $(truncate_text "$manifest" 180)"
        ;;
      *)
        die "Failed to parse qoderwake manifest with $parser_name for ${os}/${arch}"
        ;;
    esac
  fi
  IFS=$'\t' read -r url sha256 <<< "$entry"
  [[ -n "$url" ]] || die "Manifest entry for ${os}/${arch} is missing url"
  [[ -n "$sha256" ]] || die "Manifest entry for ${os}/${arch} is missing sha256"

  printf '%s|%s' "$url" "$sha256"
}

resolve_qoderwake_entry() {
  local os arch manifest

  os="$(detect_os)"
  arch="$(detect_arch "$os")"

  require_https_url "QODERWAKE_MANIFEST_URL" "$QODERWAKE_MANIFEST_URL"
  manifest="$(fetch_text_without_proxy "$QODERWAKE_MANIFEST_URL")" \
    || die "Failed to fetch qoderwake manifest from $QODERWAKE_MANIFEST_URL"

  resolve_qoderwake_entry_from_manifest "$manifest" "$os" "$arch"
}

persist_update_channel_config() {
  local manifest_url="${QODERWAKE_MANIFEST_URL:-}"
  local env_mode="${QODERWAKE_ENV_MODE:-}"

  if [[ -z "$manifest_url" && -z "$env_mode" ]]; then
    return 0
  fi

  mkdir -p "$WORK_DIR/config"
  python3 - "$WORK_DIR/config/config.json" "$manifest_url" "$env_mode" <<'PY'
import json
import os
import sys

config_path, manifest_url, env_mode = sys.argv[1:4]
update = {}
manifest_url = manifest_url.strip()
env_mode = env_mode.strip()

if manifest_url:
    if not manifest_url.startswith("https://"):
        print("WARNING: QODERWAKE_MANIFEST_URL was not persisted because it is not an HTTPS URL", file=sys.stderr)
    else:
        update["manifestUrl"] = manifest_url

if env_mode:
    normalized = env_mode.lower()
    if normalized not in {"dev", "test", "prod"}:
        print("WARNING: QODERWAKE_ENV_MODE was not persisted because it must be one of dev, test, prod", file=sys.stderr)
    else:
        update["envMode"] = normalized

if not update:
    sys.exit(0)

config = {}
if os.path.exists(config_path):
    try:
        with open(config_path, "r", encoding="utf-8") as fh:
            config = json.load(fh)
        if not isinstance(config, dict):
            raise ValueError("config root must be an object")
    except Exception as exc:
        print(f"WARNING: failed to persist qoderwake update channel config: {exc}", file=sys.stderr)
        sys.exit(0)

current_update = config.get("update")
if not isinstance(current_update, dict):
    current_update = {}
config["update"] = {**current_update, **update}
with open(config_path, "w", encoding="utf-8") as fh:
    json.dump(config, fh, indent=2, ensure_ascii=False)
    fh.write("\n")
PY
  if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" && "${SUDO_USER:-}" != "root" ]]; then
    local sudo_group
    sudo_group="$(id -gn "$SUDO_USER" 2>/dev/null || true)"
    if [[ -n "$sudo_group" ]]; then
      chown -R "$SUDO_USER:$sudo_group" "$WORK_DIR/config" 2>/dev/null \
        || log "Warning: failed to chown $WORK_DIR/config to $SUDO_USER:$sudo_group"
    else
      chown -R "$SUDO_USER" "$WORK_DIR/config" 2>/dev/null \
        || log "Warning: failed to chown $WORK_DIR/config to $SUDO_USER"
    fi
  fi
  log "Persisted qoderwake update channel config to $WORK_DIR/config/config.json"
}

load_qodercli_release_json() {
  local release_json_placeholder release_json compact manifest_url
  release_json_placeholder="__QODERCLI_RELEASE_""JSON__"

  if [[ -n "${QODERCLI_RELEASE_JSON:-}" && "$QODERCLI_RELEASE_JSON" != "$release_json_placeholder" ]]; then
    release_json="$QODERCLI_RELEASE_JSON"
  elif [[ -f "$QODERCLI_RELEASE_CONFIG_FILE" ]]; then
    release_json="$(cat "$QODERCLI_RELEASE_CONFIG_FILE")"
  else
    die "qodercli release config not found: $QODERCLI_RELEASE_CONFIG_FILE. If you are using the published install.sh, the embedded qodercli release metadata may be missing."
  fi

  compact="$(printf '%s' "$release_json" | tr -d '\n\r\t')"
  if [[ "$compact" == *'"files"'* || "$compact" == *'"archives"'* ]]; then
    printf '%s\n' "$release_json"
    return 0
  fi

  manifest_url="$(extract_json_string_field "$compact" "manifestUrl")"
  if [[ -n "$manifest_url" ]]; then
    require_https_url "QODERCLI_MANIFEST_URL" "$manifest_url"
    fetch_text_without_proxy "$manifest_url" || die "Failed to fetch qodercli manifest from $manifest_url"
    return 0
  fi

  printf '%s\n' "$release_json"
}

qodercli_release_source_label() {
  local release_json_placeholder
  release_json_placeholder="__QODERCLI_RELEASE_""JSON__"

  if [[ -n "${QODERCLI_RELEASE_JSON:-}" && "$QODERCLI_RELEASE_JSON" != "$release_json_placeholder" ]]; then
    printf 'embedded release metadata\n'
    return 0
  fi

  printf '%s\n' "$QODERCLI_RELEASE_CONFIG_FILE"
}

qodercli_archive_key() {
  local os="$1"
  local arch="$2"
  printf '%s-%s' "$os" "$arch"
}

parse_qodercli_release_with_jq() {
  local release_json="$1"
  local key="$2"
  local output

  output="$(printf '%s' "$release_json" | jq -r --arg key "$key" '
    def file_key: .os + "-" + .arch;
    if (.files | type) == "array" then
      (.latest // .version // "") as $version
      | if ($version | type) != "string" or ($version | length) == 0 then
          error("Invalid qodercli release config: missing version")
        else
          (.files[] | select(file_key == $key)) as $entry
          | if $entry == null then
              ""
            else
              [$version, ($entry.url // ""), ($entry.sha256 // "")] | @tsv
            end
        end
    elif (.version | type) != "string" then
      error("Invalid qodercli release config: missing version")
    elif (.baseUrl | type) != "string" then
      error("Invalid qodercli release config: missing baseUrl")
    elif (.archives | type) != "object" then
      error("Invalid qodercli release config: missing archives")
    else
      (.archives[$key] // null) as $entry
      | if $entry == null then
          ""
        elif (($entry.file // "") | type) != "string" or (($entry.file // "") | length) == 0 then
          error("Invalid qodercli release config: missing file for \($key)")
        else
          [.version, ((.baseUrl | sub("/$"; "")) + "/" + ($entry.file // "")), ($entry.sha256 // "")] | @tsv
        end
    end
  ')" || return 2

  [[ -n "$output" ]] || return 1
  printf '%s\n' "$output"
}

parse_qodercli_release_with_python3() {
  local release_json="$1"
  local key="$2"

  python3 -c '
import json
import sys

key = sys.argv[1]
try:
    data = json.load(sys.stdin)
except Exception as exc:
    sys.stderr.write(f"Invalid qodercli release config: {exc}\n")
    sys.exit(2)

version = data.get("version")
if not isinstance(version, str):
    version = data.get("latest")
files = data.get("files")
if isinstance(files, list):
    if not isinstance(version, str) or not version:
        sys.stderr.write("Invalid qodercli release config: missing version\n")
        sys.exit(2)
    for entry in files:
        if isinstance(entry, dict) and "{}-{}".format(entry.get("os"), entry.get("arch")) == key:
            url = entry.get("url") or ""
            sha256 = entry.get("sha256") or ""
            sys.stdout.write(f"{version}\t{url}\t{sha256}\n")
            sys.exit(0)
    sys.exit(1)

base_url = data.get("baseUrl")
archives = data.get("archives")
if not isinstance(version, str) or not isinstance(base_url, str):
    sys.stderr.write("Invalid qodercli release config: missing version/baseUrl\n")
    sys.exit(2)
if not isinstance(archives, dict):
    sys.stderr.write("Invalid qodercli release config: missing archives\n")
    sys.exit(2)
entry = archives.get(key)
if not isinstance(entry, dict):
    sys.exit(1)
file_name = entry.get("file")
sha256 = entry.get("sha256") or ""
if not isinstance(file_name, str) or not file_name:
    sys.stderr.write(f"Invalid qodercli release config: missing file for {key}\n")
    sys.exit(2)
base_url = base_url.rstrip("/")
sys.stdout.write(f"{version}\t{base_url}/{file_name}\t{sha256}\n")
' "$key" <<<"$release_json"
}

parse_qodercli_release_with_node() {
  local release_json="$1"
  local key="$2"

  node -e '
const fs = require("fs");
const key = process.argv[1];
let data;
try {
  data = JSON.parse(fs.readFileSync(0, "utf8"));
} catch (error) {
  process.stderr.write(`Invalid qodercli release config: ${error.message}\n`);
  process.exit(2);
}
if (!data || typeof data.version !== "string" || typeof data.baseUrl !== "string") {
  if (Array.isArray(data && data.files)) {
    const version = data.latest || data.version;
    if (typeof version !== "string" || !version) {
      process.stderr.write("Invalid qodercli release config: missing version\n");
      process.exit(2);
    }
    const entry = data.files.find((item) => item && `${item.os}-${item.arch}` === key);
    if (!entry) process.exit(1);
    process.stdout.write(`${version}\t${entry.url || ""}\t${entry.sha256 || ""}\n`);
    process.exit(0);
  }
  process.stderr.write("Invalid qodercli release config: missing version/baseUrl\n");
  process.exit(2);
}
if (!data.archives || typeof data.archives !== "object" || Array.isArray(data.archives)) {
  process.stderr.write("Invalid qodercli release config: missing archives\n");
  process.exit(2);
}
const entry = data.archives && data.archives[key];
if (!entry) process.exit(1);
if (!entry.file) {
  process.stderr.write(`Invalid qodercli release config: missing file for ${key}\n`);
  process.exit(2);
}
process.stdout.write(`${data.version}\t${data.baseUrl.replace(/\/$/, "")}/${entry.file}\t${entry.sha256 || ""}\n`);
' "$key" <<<"$release_json"
}

extract_json_string_field() {
  local json="$1"
  local field="$2"

  printf '%s\n' "$json" | sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p"
}

parse_qodercli_release_with_shell() {
  local release_json="$1"
  local key="$2"
  local compact version base_url archives_payload files_payload file_object archive_object file_name sha256 url

  # Portable fallback for the committed qodercli-release.json shape only:
  # top-level version/baseUrl and flat archives entries with file/sha256.
  compact="$(printf '%s' "$release_json" | tr -d '\n\r\t')"
  version="$(extract_json_string_field "$compact" "version")"
  if [[ -z "$version" ]]; then
    version="$(extract_json_string_field "$compact" "latest")"
  fi
  files_payload="$(printf '%s\n' "$compact" | sed -n 's/.*"files"[[:space:]]*:[[:space:]]*\[\(.*\)\].*/\1/p')"
  if [[ -n "$files_payload" ]]; then
    file_object="$(printf '%s\n' "$files_payload" | sed -n "s/.*\\({[^}]*\"os\"[[:space:]]*:[[:space:]]*\"${key%%-*}\"[^}]*\"arch\"[[:space:]]*:[[:space:]]*\"${key#*-}\"[^}]*}\\).*/\\1/p")"
    [[ -n "$version" ]] || return 2
    [[ -n "$file_object" ]] || return 1
    url="$(extract_json_string_field "$file_object" "url")"
    sha256="$(extract_json_string_field "$file_object" "sha256")"
    [[ -n "$url" ]] || return 2
    printf '%s\t%s\t%s\n' "$version" "$url" "$sha256"
    return 0
  fi

  base_url="$(extract_json_string_field "$compact" "baseUrl")"
  archives_payload="$(printf '%s\n' "$compact" | sed -n 's/.*"archives"[[:space:]]*:[[:space:]]*{\(.*\)}[[:space:]]*}.*/\1/p')"
  archive_object="$(printf '%s\n' "$compact" | sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*{\\([^}]*\\)}.*/\\1/p")"
  [[ -n "$version" && -n "$base_url" && -n "$archives_payload" ]] || return 2
  [[ -n "$archive_object" ]] || return 1
  file_name="$(extract_json_string_field "{$archive_object}" "file")"
  sha256="$(extract_json_string_field "{$archive_object}" "sha256")"
  [[ -n "$file_name" ]] || return 2
  printf '%s\t%s/%s\t%s\n' "$version" "${base_url%/}" "$file_name" "$sha256"
}

list_qodercli_archive_keys() {
  local release_json="$1"
  local compact archives_payload files_payload

  compact="$(printf '%s' "$release_json" | tr -d '\n\r\t')"
  files_payload="$(printf '%s\n' "$compact" | sed -n 's/.*"files"[[:space:]]*:[[:space:]]*\[\(.*\)\].*/\1/p')"
  if [[ -n "$files_payload" ]]; then
    printf '%s\n' "$files_payload" \
      | sed 's/},[[:space:]]*{/}\
{/g' \
      | while IFS= read -r entry; do
          entry_os="$(extract_json_string_field "$entry" "os")"
          entry_arch="$(extract_json_string_field "$entry" "arch")"
          if [[ -n "$entry_os" && -n "$entry_arch" ]]; then
            printf '%s-%s\n' "$entry_os" "$entry_arch"
          fi
        done \
      | join_lines_csv
    return
  fi

  archives_payload="$(printf '%s\n' "$compact" | sed -n 's/.*"archives"[[:space:]]*:[[:space:]]*{\(.*\)}[[:space:]]*}.*/\1/p')"
  [[ -n "$archives_payload" ]] || return 1

  printf '%s\n' "$archives_payload" \
    | grep -o '"[^"]*"[[:space:]]*:{' \
    | sed -E 's/^"([^"]*)".*/\1/' \
    | join_lines_csv
}

resolve_qodercli_entry() {
  local os="$1"
  local arch="$2"
  local key release_json entry version url sha256 parser_name parse_status available_keys source_label

  key="$(qodercli_archive_key "$os" "$arch")"
  release_json="$(load_qodercli_release_json)"

  parser_name="$(resolve_json_parser)"
  source_label="$(qodercli_release_source_label)"
  if capture_command_result "parse_qodercli_release_with_${parser_name}" "$release_json" "$key"; then
    entry="$CAPTURED_STDOUT"
  else
    parse_status=$?
    case "$parse_status" in
      1)
        available_keys="$(list_qodercli_archive_keys "$release_json" 2>/dev/null || true)"
        if [[ -n "$available_keys" ]]; then
          die "No qodercli package found for $key in qodercli release config ($source_label, parser: $parser_name). Available archives: $available_keys"
        fi
        die "No qodercli package found for $key in qodercli release config ($source_label, parser: $parser_name)"
        ;;
      2)
        if [[ -n "${CAPTURED_STDERR:-}" ]]; then
          die "$CAPTURED_STDERR"
        fi
        die "Failed to parse qodercli release config ($source_label, parser: $parser_name). Config preview: $(truncate_text "$release_json" 180)"
        ;;
      *)
        die "Failed to resolve qodercli release config for $key ($source_label, parser: $parser_name)"
        ;;
    esac
  fi

  IFS=$'\t' read -r version url sha256 <<< "$entry"
  [[ -n "$version" ]] || die "qodercli release entry for $key is missing version"
  [[ -n "$url" ]] || die "qodercli release entry for $key is missing url"
  [[ -n "$sha256" ]] || die "qodercli release entry for $key is missing sha256"
  require_https_url "QODERCLI_OSS_URL" "$url"

  printf '%s|%s|%s' "$version" "$url" "$sha256"
}

wait_for_qoderwake() {
  local url="$1"
  local timeout_seconds="${QODERWAKE_START_TIMEOUT_SECONDS:-60}"
  local deadline=$((SECONDS + timeout_seconds))

  while [[ $SECONDS -lt $deadline ]]; do
    if probe_url_without_proxy "$url"; then
      log "qoderwake daemon is reachable at $url"
      return 0
    fi
    sleep 2
  done

  return 1
}

start_qoderwake_daemon() {
  local qoderwake_bin="$1"
  local mode="${QODERWAKE_DAEMON_MODE:-restart}"
  local host="${QODERWAKE_HOST:-127.0.0.1}"
  local port="${QODERWAKE_PORT:-$DEFAULT_QODERWAKE_PORT}"
  local base_url="${QODERWAKE_BASE_URL:-http://$host:$port}"
  local daemon_log="${QODERWAKE_DAEMON_LOG:-$(pwd -P)/qoderwake-daemon.log}"
  local daemon_pid_file="${QODERWAKE_DAEMON_PID_FILE:-$(pwd -P)/qoderwake-daemon.pid}"
  local daemon_pid

  case "$mode" in
    restart)
      run_step "$qoderwake_bin" restart
      ;;
    foreground)
      : > "$daemon_log"
      log "Starting qoderwake daemon in foreground mode: $base_url"
      nohup "$qoderwake_bin" start --host "$host" --port "$port" --foreground >"$daemon_log" 2>&1 &
      daemon_pid=$!
      printf '%s\n' "$daemon_pid" > "$daemon_pid_file"

      if wait_for_qoderwake "$base_url"; then
        return 0
      fi

      if ! kill -0 "$daemon_pid" >/dev/null 2>&1; then
        log "qoderwake daemon process exited early; last log lines:"
      else
        log "qoderwake daemon did not become reachable; last log lines:"
      fi
      tail -n 80 "$daemon_log" >&2 || true
      return 1
      ;;
    *)
      die "Unsupported QODERWAKE_DAEMON_MODE: $mode"
      ;;
  esac
}

trim() {
  local value="$1"
  value="${value#"${value%%[![:space:]]*}"}"
  value="${value%"${value##*[![:space:]]}"}"
  printf '%s' "$value"
}

run_post_install_plugins() {
  local qoderwake_bin="$1"
  local plugins="${QODERWAKE_POST_INSTALL_PLUGINS:-}"
  [[ -n "$plugins" ]] || return 0

  local plugin plugin_name
  local IFS=","
  for plugin in $plugins; do
    plugin_name="$(trim "$plugin")"
    [[ -n "$plugin_name" ]] || continue
    run_step "$qoderwake_bin" plugin enable "$plugin_name"
  done
}

main() {
  local login_method
  login_method="BROWSER"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      -h|--help)
        usage
        exit 0
        ;;
      --login-method)
        require_arg "$1" "${2:-}"
        login_method="$2"
        shift 2
        ;;
      *)
        die "Unknown argument: $1"
        ;;
    esac
  done

  case "$login_method" in
    BROWSER|PAT) ;;
    *)
      die "Unsupported login method: $login_method. Use BROWSER or PAT"
      ;;
  esac

  case "$(uname -s | tr '[:upper:]' '[:lower:]')" in
    *mingw*|*cygwin*|*msys*)
      # Windows is intentionally unsupported by this bash installer: the
      # payload contains POSIX binaries (qoderwake, managed qodercli) and the
      # bash tail uses `ln -s` + `chmod +x`, neither of which works on
      # Windows without elevation (see
      # specs/verification/2026-05-12-windows-ci-findings.md — Issue 3/7).
      # Use the PowerShell installer instead.
      die "This bash installer targets macOS/Linux. On Windows run scripts/install.ps1 from PowerShell."
      ;;
  esac

  assert_supported_platform

  local platform_os platform_arch qodercli_url qodercli_sha256 qodercli_entry qodercli_version qoderwake_url qoderwake_sha256 qoderwake_entry
  platform_os="$(detect_os)"
  platform_arch="$(detect_arch "$platform_os")"
  log "Detected platform: ${platform_os}/${platform_arch}"
  if [[ "$platform_os" == "darwin" && "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" == "1" && "$(uname -m)" == "x86_64" ]]; then
    log "Rosetta shell detected; using Apple Silicon arm64 packages"
  fi
  warn_darwin_x64_cpu_compatibility "$platform_os" "$platform_arch"
  require_dependency python3 python3 "support later QoderWake components that require python3"

  qodercli_entry="$(resolve_qodercli_entry "$platform_os" "$platform_arch")"
  IFS='|' read -r qodercli_version qodercli_url qodercli_sha256 <<< "$qodercli_entry"
  REQUIRED_QODERCLI_VERSION="$qodercli_version"
  if [[ -n "${QODERCLI_OSS_URL:-}" ]]; then
    qodercli_url="$QODERCLI_OSS_URL"
    qodercli_sha256="${QODERCLI_OSS_SHA256:-}"
    if [[ -z "$qodercli_sha256" ]]; then
      log "WARNING: QODERCLI_OSS_URL is set without QODERCLI_OSS_SHA256; qodercli checksum verification will be skipped"
    fi
  fi
  if [[ -n "${QODERWAKE_OSS_URL:-}" ]]; then
    qoderwake_url="$QODERWAKE_OSS_URL"
    qoderwake_sha256="${QODERWAKE_OSS_SHA256:-}"
  else
    qoderwake_entry="$(resolve_qoderwake_entry)"
    qoderwake_url="${qoderwake_entry%%|*}"
    qoderwake_sha256="${qoderwake_entry#*|}"
  fi

  require_https_url "QODERCLI_OSS_URL" "$qodercli_url"
  require_https_url "QODERWAKE_OSS_URL" "$qoderwake_url"

  local project_root
  project_root="$REPO_ROOT"

  log "Creating $WORK_DIR"
  mkdir -p "$WORK_DIR"
  cd "$WORK_DIR"

  mkdir -p .tmp
  TMP_DIR="$(mktemp -d ".tmp/install.XXXXXX")"
  trap cleanup EXIT INT TERM
  local install_root qoderwake_bin qodercli_bin
  install_root="$(pwd -P)"
  qoderwake_bin="$install_root/$QODERWAKE_BINARY_NAME"
  qodercli_bin="$install_root/qodercli/$QODERCLI_BINARY_NAME"

  download_and_install "$qoderwake_url" "qoderwake" "$QODERWAKE_BINARY_NAME" "./$QODERWAKE_BINARY_NAME" "QODERWAKE_OSS_URL" "$qoderwake_sha256"
  validate_installed_binary "$qoderwake_bin" "$QODERWAKE_BINARY_NAME"

  if [[ "$BUNDLED_QODERCLI_INSTALLED" != "true" ]]; then
    if check_qodercli_version; then
      log "$QODERCLI_BINARY_NAME $REQUIRED_QODERCLI_VERSION already installed, skipping fallback download"
    else
      download_and_install "$qodercli_url" "qodercli" "qodercli" "./qodercli/$QODERCLI_BINARY_NAME" "QODERCLI_OSS_URL" "$qodercli_sha256"
    fi
  fi
  validate_installed_binary "$qodercli_bin" "$QODERCLI_BINARY_NAME"

  link_to_bin "$qodercli_bin"
  link_to_bin "$qoderwake_bin"
  ensure_in_path

  # Additionally place symlinks in a directory already in PATH (e.g. ~/.local/bin)
  # so binaries are immediately executable without restarting the shell.
  local path_link_dir
  if path_link_dir="$(resolve_path_link_dir)"; then
    link_to_path_dir "$qodercli_bin" "$path_link_dir"
    link_to_path_dir "$qoderwake_bin" "$path_link_dir"
    log "Symlinks also created in $path_link_dir (already in PATH)"
  fi

  # Ensure externally downloaded dist managed qodercli is executable (daemon may resolve to it)
  if [[ -f "$project_root/dist/$QODERCLI_BINARY_NAME" ]]; then
    chmod +x "$project_root/dist/$QODERCLI_BINARY_NAME" || log "Warning: failed to chmod dist/$QODERCLI_BINARY_NAME"
    log "Marked dist/$QODERCLI_BINARY_NAME as executable"
  fi
  persist_update_channel_config

  export QODER_ENV="${QODER_ENV:-prod}"

  log "QODER_ENV=$QODER_ENV"
  log "$QODERCLI_BINARY_NAME=$qodercli_bin"
  log "$QODERWAKE_BINARY_NAME=$qoderwake_bin"

  case "$login_method" in
    PAT)
      local qoder_pat
      qoder_pat="$(resolve_pat)"
      run_qoder_auth_step "$qoder_pat" "$qoderwake_bin" login --method token
      ;;
    BROWSER)
      run_step "$qoderwake_bin" login
      ;;
  esac
  start_qoderwake_daemon "$qoderwake_bin"
  run_post_install_plugins "$qoderwake_bin"

  # Open the console in the default browser
  local console_url="${QODERWAKE_BASE_URL:-http://${QODERWAKE_HOST:-127.0.0.1}:${QODERWAKE_PORT:-$DEFAULT_QODERWAKE_PORT}}"
  log "Installation succeeded"
  log "Login succeeded"
  log "Daemon started successfully: $console_url"
  log "Opening $console_url in default browser"
  if command -v open >/dev/null 2>&1; then
    open "$console_url" 2>/dev/null || log "Warning: failed to open browser"
  elif command -v xdg-open >/dev/null 2>&1; then
    xdg-open "$console_url" 2>/dev/null || log "Warning: failed to open browser"
  else
    log "No browser opener found; please visit $console_url manually"
  fi
}

if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]:-}" == "$0" ]]; then
  main "$@"
fi
