#!/bin/sh
set -eu

STATE_DIR="${HOME}/.local/share/nutrient"
INSTALL_DIR="${STATE_DIR}/cli"
STATE_FILE="${STATE_DIR}/pdf-to-markdown-state"
CHECK_INTERVAL_SECONDS=21600
LATEST_URL="https://agent-cdn.nutrient.io/pdf-to-markdown/LATEST"
CDN_PREFIX="https://agent-cdn.nutrient.io/pdf-to-markdown"

FORCE_FRESHNESS_CHECK=0

# BEGIN SHARED INSTALL WRAPPER
case "$(uname -s)/$(uname -m)" in
  Linux/aarch64)
    TARGET_ID="linux-arm64"
    BINARY_NAME="nutrient-linux-arm64"
    ;;
  Linux/x86_64)
    TARGET_ID="linux-amd64"
    BINARY_NAME="nutrient-linux-amd64"
    ;;
  Darwin/arm64)
    TARGET_ID="macos-arm64"
    BINARY_NAME="nutrient-macos-arm64"
    ;;
  MINGW*/x86_64|MSYS*/x86_64|CYGWIN*/x86_64)
    TARGET_ID="windows-amd64"
    BINARY_NAME="nutrient-windows-amd64.exe"
    ;;
  MINGW*/aarch64|MSYS*/aarch64|CYGWIN*/aarch64)
    TARGET_ID="windows-arm64"
    BINARY_NAME="nutrient-windows-arm64.exe"
    ;;
  *)
    echo "Unsupported platform: $(uname -s)/$(uname -m)" >&2
    exit 1
    ;;
esac

NUTRIENT_CLI="${INSTALL_DIR}/${BINARY_NAME}"
LAST_CHECKED_AT=""
RELEASE_ID=""
INSTALL_READY=0
INSTALL_LOCK_PATH="${STATE_DIR}/.install-lock"
INSTALL_LOCK_HELD=0
ARCHIVE_TMP=""
CHECKSUM_TMP=""
EXTRACT_TMP=""
NEXT_INSTALL_DIR=""
STATE_TMP=""
VERB_LINK_TMP=""

release_install_lock() {
  if [ "$INSTALL_LOCK_HELD" -eq 1 ]; then
    rm -rf "$INSTALL_LOCK_PATH" || return 1
    INSTALL_LOCK_HELD=0
  fi
}

release_install_lock_or_exit() {
  if ! release_install_lock; then
    echo "Failed to release install lock: $INSTALL_LOCK_PATH" >&2
    exit 1
  fi
}

cleanup_install_artifacts() {
  [ -z "$ARCHIVE_TMP" ] || rm -f "$ARCHIVE_TMP" || :
  [ -z "$CHECKSUM_TMP" ] || rm -f "$CHECKSUM_TMP" || :
  [ -z "$EXTRACT_TMP" ] || rm -rf "$EXTRACT_TMP" || :
  [ -z "$NEXT_INSTALL_DIR" ] || rm -rf "$NEXT_INSTALL_DIR" || :

  ARCHIVE_TMP=""
  CHECKSUM_TMP=""
  EXTRACT_TMP=""
  NEXT_INSTALL_DIR=""
}

cleanup_on_exit() {
  cleanup_install_artifacts
  [ -z "$STATE_TMP" ] || rm -f "$STATE_TMP" || :
  [ -z "$VERB_LINK_TMP" ] || rm -f "$VERB_LINK_TMP" || :
  release_install_lock || :
}

trap cleanup_on_exit EXIT
trap 'exit 1' HUP INT TERM

load_state() {
  if [ -f "$STATE_FILE" ]; then
    LAST_CHECKED_AT="$(sed -n 's/^LAST_CHECKED_AT=//p' "$STATE_FILE" | tail -n 1)"
    RELEASE_ID="$(sed -n 's/^RELEASE_ID=//p' "$STATE_FILE" | tail -n 1)"
  fi

  case "$LAST_CHECKED_AT" in
    *[!0-9]*|'') LAST_CHECKED_AT="" ;;
    *)
      LAST_CHECKED_AT="$(printf '%s\n' "$LAST_CHECKED_AT" | sed 's/^0*//')"
      [ -n "$LAST_CHECKED_AT" ] || LAST_CHECKED_AT=0
      ;;
  esac

  case "$RELEASE_ID" in
    [!A-Za-z0-9]*|*[!A-Za-z0-9T._-]*|'') RELEASE_ID="" ;;
  esac
}

write_state() {
  mkdir -p "$STATE_DIR" || return 1
  chmod go-w "$STATE_DIR" || return 1
  STATE_TMP="${STATE_FILE}.tmp.$$"

  if ! {
    printf 'LAST_CHECKED_AT=%s\n' "$1"
    printf 'RELEASE_ID=%s\n' "$2"
  } > "$STATE_TMP"; then
    rm -f "$STATE_TMP" || :
    STATE_TMP=""
    return 1
  fi

  if ! mv -f "$STATE_TMP" "$STATE_FILE"; then
    rm -f "$STATE_TMP" || :
    STATE_TMP=""
    return 1
  fi
  STATE_TMP=""
}

# Reports a binary that terminated before running a command (exit status 126 or
# higher, which includes death by signal). Shows the binary's own first error
# lines without managed stack frames, plus an install hint for missing ICU.
print_cli_start_failure() {
  failure_status="$1"
  failure_output="$2"

  echo "The Nutrient CLI could not start (exit status $failure_status):" >&2
  printf '%s\n' "$failure_output" |
    sed -e '/^[[:space:]]*at /d' -e '/^[[:space:]]*$/d' \
      -e '/^Aborted\( (core dumped)\)\{0,1\}$/d' -e '/^Killed$/d' \
      -e '/^Segmentation fault\( (core dumped)\)\{0,1\}$/d' |
    head -n 3 >&2
  case "$failure_output" in
    *ICU*|*libicu*)
      echo "Install the ICU libraries, then retry. Debian or Ubuntu: apt-get install libicu-dev. Alpine: apk add icu-libs. Fedora or RHEL: dnf install libicu." >&2
      ;;
  esac
}

have_installed_binary() {
  [ -x "$NUTRIENT_CLI" ]
}

download_to_stdout() {
  url="$1"

  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --proto '=https' --proto-redir '=https' \
      --connect-timeout 5 --max-time 30 --retry 2 "$url" || return 1
    return 0
  fi

  if command -v wget >/dev/null 2>&1; then
    wget -q --https-only --timeout=30 --tries=2 -O - "$url" || return 1
    return 0
  fi

  echo "Neither curl nor wget is available for downloading files" >&2
  return 1
}

download_to_file() {
  url="$1"
  destination="$2"
  max_time="${3:-300}"

  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --proto '=https' --proto-redir '=https' \
      --connect-timeout 5 --max-time "$max_time" --retry 2 \
      "$url" -o "$destination" || return 1
    return 0
  fi

  if command -v wget >/dev/null 2>&1; then
    wget -q --https-only --timeout=30 --tries=2 \
      -O "$destination" "$url" || return 1
    return 0
  fi

  echo "Neither curl nor wget is available for downloading files" >&2
  return 1
}

fetch_latest_release_id() {
  latest="$(download_to_stdout "$LATEST_URL")" || return 1
  latest="$(printf '%s' "$latest" | tr -d '\r' | sed -e 's/[[:space:]]*$//')"

  case "$latest" in
    [!A-Za-z0-9]*|*[!A-Za-z0-9T._-]*|'') return 1 ;;
    *) printf '%s' "$latest" ;;
  esac
}

archive_url_for_release() {
  release_id="$1"

  case "$release_id" in
    [!A-Za-z0-9]*|*[!A-Za-z0-9T._-]*|'') return 1 ;;
    *) printf '%s/%s/%s.tar.gz' "$CDN_PREFIX" "$release_id" "$TARGET_ID" ;;
  esac
}

selected_binary_exists() {
  selected_binary="$1"
  [ -x "$selected_binary" ]
}

validate_extract_contents() {
  extract_dir="$1"
  selected_binary="$2"

  [ -f "$selected_binary" ] || return 1
  [ ! -L "$selected_binary" ] || return 1
  [ -x "$selected_binary" ] || return 1
  [ "$(basename "$selected_binary")" = "$(basename "$NUTRIENT_CLI")" ] || return 1

  found_count="$(
    find "$extract_dir" ! -path "$extract_dir" -print | wc -l | tr -d ' '
  )" || return 1
  case "$found_count" in
    1) ;;
    *) return 1 ;;
  esac
}

compute_sha256() {
  checksum_file="$1"
  checksum_output=""

  if command -v sha256sum >/dev/null 2>&1; then
    checksum_output="$(sha256sum "$checksum_file" 2>/dev/null)" || return 1
    checksum_hash="$(printf '%s\n' "$checksum_output" | awk '{ print $1 }')"
  elif command -v shasum >/dev/null 2>&1; then
    checksum_output="$(shasum -a 256 "$checksum_file" 2>/dev/null)" || return 1
    checksum_hash="$(printf '%s\n' "$checksum_output" | awk '{ print $1 }')"
  elif command -v openssl >/dev/null 2>&1; then
    checksum_output="$(openssl dgst -sha256 "$checksum_file" 2>/dev/null)" || return 1
    checksum_hash="$(printf '%s\n' "$checksum_output" | awk '{ print $NF }')"
  else
    return 2
  fi

  case "$checksum_hash" in
    *[!A-Fa-f0-9]*|'') return 1 ;;
  esac
  [ "${#checksum_hash}" -eq 64 ] || return 1
  printf '%s' "$checksum_hash"
}

verify_checksum_if_present() {
  archive_url="$1"
  archive_file="$2"
  checksum_url="${archive_url}.sha256"

  CHECKSUM_TMP="$(mktemp "${TMPDIR:-/tmp}/pdf-to-markdown.sha256.XXXXXX")" ||
    return 1

  if download_to_file "$checksum_url" "$CHECKSUM_TMP" 30 2>/dev/null; then
    expected_checksum="$(
      awk '{
        for (i = 1; i <= NF; i++) {
          if (length($i) == 64 && $i !~ /[^A-Fa-f0-9]/) {
            print $i
            exit
          }
        }
      }' "$CHECKSUM_TMP"
    )" || return 1

    if [ -z "$expected_checksum" ]; then
      echo "Checksum sidecar does not contain a SHA-256 value: $checksum_url" >&2
      return 1
    fi

    checksum_status=0
    actual_checksum="$(compute_sha256 "$archive_file")" || checksum_status=$?
    if [ "$checksum_status" -eq 2 ]; then
      if [ "${NUTRIENT_REQUIRE_CHECKSUM:-0}" = "1" ]; then
        echo "SHA-256 verification is required, but no checksum tool is available" >&2
        return 1
      fi
      return 0
    fi
    if [ "$checksum_status" -ne 0 ]; then
      echo "Failed to compute SHA-256 for downloaded archive" >&2
      return 1
    fi

    expected_checksum="$(printf '%s' "$expected_checksum" | tr 'A-F' 'a-f')"
    actual_checksum="$(printf '%s' "$actual_checksum" | tr 'A-F' 'a-f')"
    if [ "$actual_checksum" != "$expected_checksum" ]; then
      echo "Checksum mismatch for downloaded archive" >&2
      return 1
    fi
    return 0
  fi

  if [ "${NUTRIENT_REQUIRE_CHECKSUM:-0}" = "1" ]; then
    echo "Required checksum sidecar is unavailable: $checksum_url" >&2
    return 1
  fi
  return 0
}

install_archive() {
  archive_url="$1"
  ARCHIVE_TMP="$(mktemp "${TMPDIR:-/tmp}/pdf-to-markdown.XXXXXX")" ||
    return 1
  EXTRACT_TMP="$(mktemp -d "${TMPDIR:-/tmp}/pdf-to-markdown.XXXXXX")" || {
    cleanup_install_artifacts
    return 1
  }
  NEXT_INSTALL_DIR="${INSTALL_DIR}/.new.$$"

  download_to_file "$archive_url" "$ARCHIVE_TMP" 300 || {
    cleanup_install_artifacts
    return 1
  }
  verify_checksum_if_present "$archive_url" "$ARCHIVE_TMP" || {
    cleanup_install_artifacts
    return 1
  }
  tar -xzf "$ARCHIVE_TMP" -C "$EXTRACT_TMP" || {
    cleanup_install_artifacts
    return 1
  }

  selected_extract_binary="${EXTRACT_TMP}/$(basename "$NUTRIENT_CLI")"
  validate_extract_contents "$EXTRACT_TMP" "$selected_extract_binary" || {
    echo "Installed archive is missing the expected binary" >&2
    cleanup_install_artifacts
    return 1
  }

  mkdir -p "$STATE_DIR" || {
    cleanup_install_artifacts
    return 1
  }
  chmod go-w "$STATE_DIR" || {
    cleanup_install_artifacts
    return 1
  }
  if ! mkdir -p "$INSTALL_DIR" || ! chmod go-w "$INSTALL_DIR"; then
    cleanup_install_artifacts
    return 1
  fi
  rm -rf "$NEXT_INSTALL_DIR" || {
    cleanup_install_artifacts
    return 1
  }
  mkdir -p "$NEXT_INSTALL_DIR" || {
    cleanup_install_artifacts
    return 1
  }
  chmod go-w "$NEXT_INSTALL_DIR" || {
    cleanup_install_artifacts
    return 1
  }
  cp "$selected_extract_binary" \
    "$NEXT_INSTALL_DIR/$(basename "$NUTRIENT_CLI")" || {
    cleanup_install_artifacts
    return 1
  }

  smoke_status=0
  "$NEXT_INSTALL_DIR/$(basename "$NUTRIENT_CLI")" --version \
    >/dev/null 2>&1 || smoke_status=$?
  if [ "$smoke_status" -ge 126 ]; then
    case "$TARGET_ID/$smoke_status" in
      windows-*/126|windows-*/127) ;;
      *)
        echo "Downloaded binary failed its execution smoke test" >&2
        cleanup_install_artifacts
        return 1
        ;;
    esac
  fi

  # --version does not start the .NET runtime. On Linux, probe a command that
  # does, so a host missing a runtime dependency (for example the ICU libraries)
  # is reported once at install time instead of as an unexplained crash later.
  # The install still completes: --version, --help, and --license keep working.
  case "$TARGET_ID" in
    linux-*)
      probe_status=0
      probe_output="$({ "$NEXT_INSTALL_DIR/$(basename "$NUTRIENT_CLI")" auth --help; } 2>&1)" ||
        probe_status=$?
      if [ "$probe_status" -ge 126 ]; then
        print_cli_start_failure "$probe_status" "$probe_output"
      fi
      ;;
  esac

  # The archive contains only this binary. Replace it with one same-filesystem
  # rename, leaving the directory and command links available to cached callers.
  # Readers can execute either complete version; there is no missing-file window.
  mv -f "$NEXT_INSTALL_DIR/$BINARY_NAME" "$NUTRIENT_CLI" || {
    cleanup_install_artifacts
    return 1
  }

  cleanup_install_artifacts
}

lock_mtime() {
  lock_dir="$1"
  lock_timestamp=""

  lock_timestamp="$(stat -c %Y "$lock_dir" 2>/dev/null)" ||
    lock_timestamp="$(stat -f %m "$lock_dir" 2>/dev/null)" ||
    return 1
  case "$lock_timestamp" in
    *[!0-9]*|'') return 1 ;;
  esac
  printf '%s' "$lock_timestamp"
}

acquire_install_lock() {
  lock_attempts=0

  while ! mkdir "$INSTALL_LOCK_PATH" 2>/dev/null; do
    if [ -d "$INSTALL_LOCK_PATH" ]; then
      existing_lock_mtime="$(lock_mtime "$INSTALL_LOCK_PATH")" ||
        existing_lock_mtime=""
      lock_now="$(date +%s)" || return 1
      case "$lock_now" in
        *[!0-9]*|'') return 1 ;;
      esac

      if [ -n "$existing_lock_mtime" ] &&
        [ "$lock_now" -gt "$((existing_lock_mtime + 600))" ]; then
        rm -rf "$INSTALL_LOCK_PATH" || return 1
        continue
      fi
    fi

    # Only a first install needs to wait for the updater. A cached command can
    # run safely while the replacement is downloaded and atomically installed.
    if have_installed_binary; then
      return 1
    fi

    lock_attempts=$((lock_attempts + 1))
    if [ "$lock_attempts" -ge 60 ]; then
      return 1
    fi
    sleep 1 || return 1
  done

  INSTALL_LOCK_HELD=1
}

release_is_older() {
  candidate_release="$1"
  installed_release="$2"

  [ "$candidate_release" != "$installed_release" ] || return 1
  first_release="$(
    printf '%s\n%s\n' "$candidate_release" "$installed_release" |
      LC_ALL=C sort |
      sed -n '1p'
  )" || return 1
  [ "$first_release" = "$candidate_release" ]
}

refresh_if_needed() {
  mkdir -p "$STATE_DIR" || {
    echo "Failed to create state directory" >&2
    exit 1
  }
  chmod go-w "$STATE_DIR" || {
    echo "Failed to secure state directory permissions" >&2
    exit 1
  }

  if ! acquire_install_lock; then
    if have_installed_binary; then
      return 0
    fi
    echo "Timed out waiting for install lock: $INSTALL_LOCK_PATH" >&2
    exit 1
  fi

  now="$(date +%s)" || {
    echo "Failed to read the current time" >&2
    exit 1
  }
  case "$now" in
    *[!0-9]*|'')
      echo "Current time is not a Unix timestamp" >&2
      exit 1
      ;;
  esac
  load_state
  INSTALL_READY=0

  if [ -e "$INSTALL_DIR" ] || [ -L "$INSTALL_DIR" ]; then
    chmod go-w "$INSTALL_DIR" || {
      echo "Failed to secure install directory permissions" >&2
      exit 1
    }
  fi

  # Account commands check the release channel whenever no updater is active.
  # The dispatch guard below also rejects an incompatible cached binary.
  if [ "$FORCE_FRESHNESS_CHECK" -eq 1 ]; then
    LAST_CHECKED_AT=""
  fi

  if have_installed_binary; then
    INSTALL_READY=1
  fi

  should_check=1
  if [ "$INSTALL_READY" -eq 1 ] && [ -n "$LAST_CHECKED_AT" ]; then
    next_check_at=$((LAST_CHECKED_AT + CHECK_INTERVAL_SECONDS))
    if [ "$now" -lt "$next_check_at" ]; then
      should_check=0
    fi
  fi

  if [ "$INSTALL_READY" -eq 1 ] && [ "$should_check" -eq 0 ]; then
    release_install_lock_or_exit
    return 0
  fi

  latest_release_id="$(fetch_latest_release_id)" || {
    if [ "$INSTALL_READY" -eq 1 ]; then
      release_install_lock_or_exit
      return 0
    fi
    echo "Failed to fetch latest pdf-to-markdown release id" >&2
    exit 1
  }

  if [ -n "$RELEASE_ID" ] &&
    release_is_older "$latest_release_id" "$RELEASE_ID"; then
    echo "downgrading from $RELEASE_ID to $latest_release_id (CDN rollback?)" >&2
  fi

  if [ "$INSTALL_READY" -eq 1 ] && [ "$latest_release_id" = "$RELEASE_ID" ]; then
    if ! write_state "$now" "$latest_release_id"; then
      release_install_lock_or_exit
      return 1
    fi
    release_install_lock_or_exit
    return 0
  fi

  latest_archive_url="$(archive_url_for_release "$latest_release_id")" || {
    if [ "$INSTALL_READY" -eq 1 ]; then
      release_install_lock_or_exit
      return 0
    fi
    echo "Failed to build archive URL for release $latest_release_id" >&2
    exit 1
  }

  install_status=0
  install_archive "$latest_archive_url" || install_status=$?

  if [ "$install_status" -ne 0 ]; then
    if have_installed_binary; then
      release_install_lock_or_exit
      return 0
    fi
    echo "Failed to install pdf-to-markdown archive for $TARGET_ID" >&2
    exit 1
  fi

  if ! write_state "$now" "$latest_release_id"; then
    release_install_lock_or_exit
    return 1
  fi
  release_install_lock_or_exit
}

refresh_if_needed

if ! selected_binary_exists "$NUTRIENT_CLI"; then
  echo "Binary not found or install is incomplete: $NUTRIENT_CLI" >&2
  exit 1
fi
# END SHARED INSTALL WRAPPER

# nutrient 1.1.0+ is multi-call: it reads its verb from argv[0]'s basename
# (pdf-to-markdown / pdf-to-text / query / self-update). Pin the verb explicitly
# by exec'ing a `query` symlink next to the platform binary, so the same shared
# binary that backs pdf-to-markdown runs the query verb here.
#
# `query` is a two-level verb: the binary expects `query text INPUT "QUERY"`.
# The wrapper passes arguments through unchanged, so callers write the `text`
# sub-verb explicitly (e.g. `query text output.md "your question"`). Keeping it
# explicit means the typed command, the binary's --help, and the sub-verb
# namespace (text today, more later) all agree.
VERB_LINK="${INSTALL_DIR}/query"

verb_link_is_valid() {
  [ -e "$VERB_LINK" ] || return 1
  verb_link_target="$(readlink "$VERB_LINK" 2>/dev/null)" ||
    verb_link_target=""

  if [ "$verb_link_target" = "$BINARY_NAME" ]; then
    return 0
  fi

  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*)
      # Git Bash can emulate symlinks with copies. Compare their contents:
      # a replacement binary can have an older timestamp than an existing copy.
      [ -z "$verb_link_target" ] &&
        cmp -s "$NUTRIENT_CLI" "$VERB_LINK"
      ;;
    *) return 1 ;;
  esac
}

cached_verb_link_is_usable() {
  verb_link_is_valid && return 0
  case "$(uname -s)" in
    # Windows may refuse to replace a running command copy. It remains usable
    # until it exits; a subsequent invocation will update it.
    MINGW*|MSYS*|CYGWIN*) [ -f "$VERB_LINK" ] && [ ! -L "$VERB_LINK" ] && [ -x "$VERB_LINK" ] ;;
    *) return 1 ;;
  esac
}

if ! verb_link_is_valid; then
  VERB_LINK_TMP="${VERB_LINK}.tmp.$$"
  rm -f "$VERB_LINK_TMP" || :
  if ! ln -s "$BINARY_NAME" "$VERB_LINK_TMP"; then
    rm -f "$VERB_LINK_TMP" || :
    VERB_LINK_TMP=""
    if ! cached_verb_link_is_usable; then
      echo "Failed to create command link: $VERB_LINK" >&2
      exit 1
    fi
  elif ! mv -f "$VERB_LINK_TMP" "$VERB_LINK"; then
    rm -f "$VERB_LINK_TMP" || :
    VERB_LINK_TMP=""
    if ! cached_verb_link_is_usable; then
      echo "Failed to install command link: $VERB_LINK" >&2
      exit 1
    fi
  else
    VERB_LINK_TMP=""
  fi
fi

exec "$VERB_LINK" "$@"
