#!/bin/sh
# Qodo CLI installer — https://get.qodo.ai/install.sh
#
#   curl -fsSL https://get.qodo.ai | sh                 # default channel (next, pre-GA)
#   curl -fsSL https://get.qodo.ai | sh -s -- --latest  # stable channel
#   curl -fsSL https://get.qodo.ai | sh -s -- --next    # prerelease channel (explicit)
#
# No npm: fetches version.json from the public bucket, downloads the bundled
# single-file CLI (qodo.mjs) for the chosen channel, verifies its sha256, and
# installs it user-owned under ~/.qodo/bin with a `qodo` launcher shim — so the
# auto-updater never needs sudo. POSIX sh; no bashisms, no eval. Pre-GA the
# default channel is `next` (there is no `latest` yet; flip DEFAULT_CHANNEL at
# GA). The whole script is wrapped in main() and only invoked on the last line,
# so a connection dropped mid-download can't execute a partial script (standard
# curl|sh hardening).
set -eu

main() {
  err()  { echo "qodo install: $*" >&2; exit 1; }
  have() { command -v "$1" >/dev/null 2>&1; }

  # Public distribution is get.qodo.ai — the branded CDN in front of the prod bucket.
  # Internal testing pulls the DEV bucket (no CDN) via override:
  #   QODO_INSTALL_BASE=https://storage.googleapis.com/qodo-cli-public-dev sh install.sh
  # src/update.ts BASE_URL default MUST match this — keep in sync so an install and its
  # later auto-updates pull from the same place. Pre-GA/GA differ only in the CHANNEL,
  # not the host: at GA flip DEFAULT_CHANNEL next→latest.
  BASE="${QODO_INSTALL_BASE:-https://get.qodo.ai}"
  DEFAULT_CHANNEL="next"   # flip to "latest" at GA (bucket is already prod)
  MIN_NODE_MAJOR=20
  # Guard HOME explicitly: under `set -u` a bare $HOME expansion would abort with
  # a cryptic "parameter not set" instead of our own message.
  [ -n "${QODO_HOME:-}" ] || [ -n "${HOME:-}" ] || err "neither QODO_HOME nor HOME is set."
  QODO_HOME="${QODO_HOME:-$HOME/.qodo}"
  BIN_DIR="$QODO_HOME/bin"

  channel="$DEFAULT_CHANNEL"
  for arg in "$@"; do
    case "$arg" in
      --next)        channel="next" ;;
      --latest)      channel="latest" ;;
      --channel=*)   channel="${arg#--channel=}" ;;
      -h|--help)
        echo "Usage: install.sh [--next|--latest|--channel=<name>]"
        return 0 ;;
      *)
        echo "qodo install: unknown option '$arg'" >&2
        return 2 ;;
    esac
  done

  # Validate the channel before it becomes part of a URL / JSON key lookup. Allow
  # only name-ish tokens: alnum, dot, hyphen, underscore.
  case "$channel" in
    '' )            err "empty --channel; use a channel like 'next' or 'latest'." ;;
    -* )            err "invalid --channel '$channel' (must not start with '-')." ;;
    *[!A-Za-z0-9._-]* ) err "invalid --channel '$channel' (allowed: letters, digits, . _ -)." ;;
  esac

  # --- prerequisites --------------------------------------------------------
  have node || err "Node.js >= ${MIN_NODE_MAJOR} is required but 'node' was not found.
  Install Node from https://nodejs.org/ (or nvm) and re-run."

  # Downloader: curl (the canonical curl|sh path) with a wget fallback so
  # `sh install.sh` still works on wget-only minimal images.
  if have curl; then
    fetch()    { curl -fsSL "$1"; }
    fetch_to() { curl -fsSL "$1" -o "$2"; }
  elif have wget; then
    fetch()    { wget -qO- "$1"; }
    fetch_to() { wget -qO "$2" "$1"; }
  else
    err "curl or wget is required but neither was found."
  fi

  node_major=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)
  case "$node_major" in
    ''|*[!0-9]*) err "could not determine the Node.js version from 'node'." ;;
  esac
  [ "$node_major" -ge "$MIN_NODE_MAJOR" ] || \
    err "Node.js >= ${MIN_NODE_MAJOR} is required, but found v$(node -v | tr -d 'v ')."

  # sha256 tool: Linux has sha256sum, macOS has shasum, minimal envs have openssl.
  # Output normalized to a bare lowercase hex digest.
  if have sha256sum; then
    sha256() { sha256sum "$1" | cut -d' ' -f1 | tr 'A-F' 'a-f'; }
  elif have shasum; then
    sha256() { shasum -a 256 "$1" | cut -d' ' -f1 | tr 'A-F' 'a-f'; }
  elif have openssl; then
    sha256() { openssl dgst -sha256 "$1" | sed 's/.*= *//' | tr 'A-F' 'a-f'; }
  else
    err "need sha256sum, shasum, or openssl to verify the download."
  fi

  # --- resolve the release from version.json --------------------------------
  echo "qodo install: fetching ${BASE}/version.json…" >&2
  version_json=$(fetch "${BASE}/version.json") || \
    err "could not fetch ${BASE}/version.json (network, or channel not published yet)."

  # Pull this channel's {version,url,sha256} via node (already a hard dep) —
  # robust JSON, no jq. Prints three lines; empty channel → exit 3.
  fields=$(printf '%s' "$version_json" | node -e '
    let s = ""; process.stdin.on("data", d => s += d).on("end", () => {
      const ch = process.argv[1];
      let j; try { j = JSON.parse(s); } catch { process.exit(4); }
      const c = j && j.channels && j.channels[ch];
      if (!c || !c.version || !c.url || !c.sha256) process.exit(3);
      process.stdout.write(c.version + "\n" + c.url + "\n" + c.sha256 + "\n");
    });
  ' "$channel") || {
    rc=$?
    [ "$rc" -eq 3 ] && err "channel '$channel' is not published yet. Pre-GA, try --next."
    err "version.json is malformed or unreadable."
  }

  version=$(printf '%s' "$fields" | sed -n '1p')
  rel_url=$(printf '%s' "$fields" | sed -n '2p')
  want_sha=$(printf '%s' "$fields" | sed -n '3p' | tr 'A-F' 'a-f')
  echo "qodo install: channel '$channel' -> ${version}" >&2

  # version.json is trusted (sha is read from the same doc), but defend against
  # malformed metadata: the artifact url MUST be a safe relative path, not an
  # absolute URL, scheme, leading slash, or parent-dir traversal.
  case "$rel_url" in
    ''|/*|*://*|*..*|*' '*) err "refusing unsafe artifact url in version.json: '$rel_url'." ;;
  esac
  case "$want_sha" in
    *[!0-9a-f]*|'') err "version.json sha256 is not a lowercase hex digest." ;;
  esac
  [ "${#want_sha}" -eq 64 ] || err "version.json sha256 is not 64 hex chars."

  # --- remove the deprecated @qodo/command npm global ----------------------
  # The old CLI shipped only as an npm global and owns a `qodo` binary; left
  # installed, it can sit ahead on PATH and shadow this install. Do this BEFORE
  # writing our own launcher: if the user's npm global bin overlaps $BIN_DIR
  # (e.g. `npm config set prefix ~/.qodo`), `npm rm` would otherwise delete the
  # freshly written shim. `npm rm` is a silent no-op (exit 0) when the package
  # is absent, so run it unconditionally rather than gating on `npm ls` — whose
  # non-zero exit on any unrelated global-tree issue would wrongly skip a real
  # removal. The `npm ls` here only decides the informational message; it never
  # blocks the removal. A real failure (e.g. EACCES on a system node) falls back
  # to a manual hint.
  if have npm; then
    if npm ls -g --depth=0 @qodo/command >/dev/null 2>&1; then
      echo "qodo install: removing the deprecated @qodo/command npm package…" >&2
    fi
    npm rm -g @qodo/command >/dev/null 2>&1 \
      || echo "qodo install: couldn't remove @qodo/command — run 'npm rm -g @qodo/command' yourself." >&2
  fi

  # --- download + verify ----------------------------------------------------
  mkdir -p "$BIN_DIR"
  # Temp file in the SAME dir as the target so the final mv is atomic (same fs).
  tmp="$BIN_DIR/.qodo.mjs.$$"
  trap 'rm -f "$tmp"' EXIT
  printf '\n==> Downloading qodo v%s (%s channel)\n' "$version" "$channel" >&2
  fetch_to "${BASE}/${rel_url}" "$tmp" || err "download failed: ${BASE}/${rel_url}"

  got_sha=$(sha256 "$tmp")
  [ "$got_sha" = "$want_sha" ] || \
    err "sha256 mismatch (want ${want_sha}, got ${got_sha}). Aborting — file may be corrupt or tampered."
  printf '%s: OK\n' "$(basename "$rel_url")" >&2

  # --- install (atomic) -----------------------------------------------------
  mv -f "$tmp" "$BIN_DIR/qodo.mjs"
  trap - EXIT

  # Launcher shim on PATH — runs the bundled mjs under the user's node.
  # The node path is resolved NOW and baked in absolute: GUI-launched agents
  # (e.g. the Claude Code desktop app) run shells with a minimal PATH that has
  # neither ~/.qodo/bin nor homebrew/nvm node, so a bare `exec node` breaks
  # there. Fallback to PATH lookup covers the baked node being removed later
  # (nvm uninstall etc.).
  # Two baked candidates, tried in order, because they fail differently:
  #  - process.execPath is the REAL binary — survives a minimal PATH even for
  #    asdf-style shims, but is version-pinned (goes stale on e.g. brew upgrade).
  #  - `command -v node` is the stable launcher path (symlink/shim) — survives
  #    upgrades, but a version-manager shim re-needs its manager on PATH.
  # Last resort: bare `node` from whatever PATH the caller has. Either
  # candidate can come back RELATIVE (relative PATH entry) — useless baked
  # into a shim that runs from any cwd — so keep only absolute ones; empty
  # degrades cleanly through the fallback chain.
  node_real=$(node -p 'process.execPath' 2>/dev/null) || node_real=""
  node_path=$(command -v node) || err "node vanished mid-install."
  case "$node_real" in /*) ;; *) node_real="" ;; esac
  case "$node_path" in /*) ;; *) node_path="" ;; esac
  [ -n "$node_real" ] || node_real="$node_path"
  cat > "$BIN_DIR/qodo" <<EOF
#!/bin/sh
NODE="$node_real"
[ -x "\$NODE" ] || NODE="$node_path"
[ -x "\$NODE" ] || NODE=node
exec "\$NODE" "$BIN_DIR/qodo.mjs" "\$@"
EOF
  chmod +x "$BIN_DIR/qodo"

  # --- PATH wiring ----------------------------------------------------------
  case ":$PATH:" in
    *":$BIN_DIR:"*) ;;   # already on PATH, nothing to do
    *)
      hint="export PATH=\"$BIN_DIR:\$PATH\""
      # The APPENDED line guards at run time against the dir already being on
      # PATH — so a login file that sources .bashrc (Debian's default .profile
      # does) can't double-prepend.
      line="case \":\$PATH:\" in *\":$BIN_DIR:\"*) ;; *) export PATH=\"$BIN_DIR:\$PATH\" ;; esac"
      # Idempotent append — don't duplicate the line on re-install.
      wire_rc() {
        if [ ! -f "$1" ] || ! grep -qF "$BIN_DIR" "$1"; then
          printf '\n# Added by the Qodo CLI installer\n%s\n' "$line" >> "$1"
          echo "qodo install: added $BIN_DIR to PATH in $1" >&2
        else
          echo "qodo install: PATH already wired in $1" >&2
        fi
      }
      # HOME may legitimately be unset when QODO_HOME is (the prerequisites
      # guard accepts either) — and rc files live under $HOME, so without it
      # there is nothing to wire: print the manual step instead of tripping
      # set -u on the expansions below.
      if [ -z "${HOME:-}" ]; then
        echo "qodo install: HOME is not set — add $BIN_DIR to your PATH:" >&2
        echo "  $hint" >&2
      else
        case "${SHELL:-}" in
          */zsh)  wire_rc "$HOME/.zshrc" ;;
          */bash)
            # Interactive shells read .bashrc; login shells (ssh, console) read
            # exactly ONE of .bash_profile / .bash_login / .profile — the first
            # that exists (bash's own lookup order). Wire .bashrc plus that one
            # (defaulting to creating .profile when none exist yet).
            wire_rc "$HOME/.bashrc"
            login_rc="$HOME/.profile"
            for f in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
              if [ -f "$f" ]; then login_rc="$f"; break; fi
            done
            wire_rc "$login_rc"
            ;;
          *)
            echo "qodo install: add $BIN_DIR to your PATH:" >&2
            echo "  $hint" >&2
            ;;
        esac
        case "${SHELL:-}" in
          */zsh|*/bash) echo "qodo install: restart your shell or run: $hint" >&2 ;;
        esac
      fi
      ;;
  esac

  # --- D15 PATH-shadow warning ---------------------------------------------
  # Any other `qodo` (the npm @qodo/command is handled above) may sit earlier on PATH.
  existing=$(command -v qodo 2>/dev/null || true)
  if [ -n "$existing" ] && [ "$existing" != "$BIN_DIR/qodo" ]; then
    echo "qodo install: warning — another 'qodo' is ahead on PATH: $existing" >&2
    echo "qodo install: ensure $BIN_DIR comes first (it was prepended above)." >&2
  fi

  # --- verify + branded banner ---------------------------------------------
  installed=$("$BIN_DIR/qodo" --version 2>/dev/null || echo "$version")

  # Post-install banner (stderr, like every other line here). Heredoc keeps the
  # logo + aligned command columns byte-exact; ${installed}/${BIN_DIR} expand.
  cat >&2 <<EOF

                      ██
                      ██
  ██████  ██████  ██████  ██████
  ██  ██  ██  ██  ██  ██  ██  ██
  ██  ██  ██  ██  ██  ██  ██  ██
  ██████  ██████  ██████  ██████
      ██
      ██

  Qodo CLI (Beta)

  Installed qodo ${installed} to ${BIN_DIR}/qodo
EOF

  # Smooth onboarding: run setup (install skills into the coding agents, then
  # offer login) as part of install — no separate step. Interactive when a
  # terminal is available: curl|sh keeps one on /dev/tty even though stdin is the
  # pipe (the rustup trick). Scripted/CI installs (no tty) skip it and get the
  # commands printed below.
  # `if cmd` swallows the non-zero for `set -e` but lets us branch on it — so a
  # failed setup is surfaced (below), not hidden behind a clean banner.
  setup_ok=0
  if [ -t 1 ] && [ -r /dev/tty ]; then
    printf '\n' >&2
    if "$BIN_DIR/qodo" setup < /dev/tty; then setup_ok=1; fi
  fi

  cat >&2 <<EOF

  Now just talk to your coding agent — it drives Qodo for you. Try asking:

    "Why did checkout start failing last week?"
    "Which repos break if I change the billing API?"
    "How does auth work in the payments service?"
    "How did we add rate limiting before — and what would it touch here?"
    "Resolve the action-required findings on my PR <url>"
    "Watch my PR and fix the review findings until it's clean"

  (qodo --help lists the raw commands; qodo skills manages the agent skills.)
EOF

  # Setup skipped (non-interactive) or failed → show the manual steps to finish.
  if [ "$setup_ok" != "1" ]; then
    printf '\n  Finish setup:\n\n    qodo skills install     # add skills to your coding agents\n    qodo login              # connect your Qodo account\n' >&2
  fi
}

main "$@"
