#!/bin/sh
# Qodo CLI installer — https://get.qodo.ai/install.sh
#
# Download this file, verify the SHA-256 pinned in README.md, then run it:
#   sh install.sh                 # default channel (next, pre-GA)
#   sh install.sh --latest        # stable channel
#   sh install.sh --next          # prerelease channel (explicit)
#   sh install.sh --auth-url https://platform.acme.internal
#                                 # authenticate against that deployment
#                                 # (or $QODO_AUTH_URL)
#
# No npm: fetches version.json from the configured distribution origin, 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.
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
  # This exact origin is persisted after verification so later manual/background
  # updates stay on the same deployment boundary (notably the on-prem QAR mirror).
  BASE="${QODO_INSTALL_BASE:-https://get.qodo.ai}"
  DEFAULT_CHANNEL="next"   # flip to "latest" at GA (bucket is already prod)
  MIN_NODE_VERSION=20.6.0
  # 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"
  # Auth endpoint for the login that `qodo setup` runs below: a portal hands out an
  # install command carrying its own deployment URL, so the install authenticates
  # against that platform instead of cloud. $QODO_AUTH_URL is the env form (for
  # callers that can't pass arguments); an explicit flag wins over it.
  auth_url="${QODO_AUTH_URL:-}"
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --next)        channel="next" ;;
      --latest)      channel="latest" ;;
      --channel=*)   channel="${1#--channel=}" ;;
      # Both spellings: a generated command may use either.
      --auth-url=*)  auth_url="${1#--auth-url=}" ;;
      --auth-url)
        shift
        [ "$#" -gt 0 ] || err "--auth-url needs a URL."
        auth_url="$1" ;;
      -h|--help)
        echo "Usage: install.sh [--next|--latest|--channel=<name>] [--auth-url <url>]"
        return 0 ;;
      *)
        echo "qodo install: unknown option '$1'" >&2
        return 2 ;;
    esac
    shift
  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

  # Reject a malformed auth URL here rather than passing it on: the CLI validates
  # it properly (URL parsing), but failing at the install line is a clearer error
  # than one surfacing inside setup. Mirrors the CLI's rule — https, except
  # loopback — plus a URL-character check so nothing shell-ish rides along.
  if [ -n "$auth_url" ]; then
    case "$auth_url" in
      https://*) ;;
      http://localhost|http://localhost:*|http://localhost/*) ;;
      http://127.0.0.1|http://127.0.0.1:*|http://127.0.0.1/*) ;;
      *) err "invalid --auth-url '$auth_url': must be https:// (http:// only for localhost)." ;;
    esac
    case "$auth_url" in
      *[!A-Za-z0-9:/._~%+-]*) err "invalid --auth-url '$auth_url': unexpected characters." ;;
    esac
  fi

  # This is a user-scoped install. `sudo` can either redirect HOME to root's
  # account or leave root-owned files under the caller's HOME, making the next
  # normal update fail. Containers may intentionally run as root, so warn rather
  # than rejecting the invocation outright.
  if have id && [ "$(id -u 2>/dev/null || echo unknown)" = "0" ]; then
    echo "qodo install: warning — running this user-scoped installer as root writes to '$QODO_HOME'." >&2
    echo "qodo install: prefer the intended user; sudo can create files that user cannot update." >&2
  fi

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

  # Downloader: curl with a wget fallback so the verified installer 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_supported=$(node -p '
    const [major, minor] = process.versions.node.split(".").map(Number);
    Number(major > 20 || (major === 20 && minor >= 6));
  ' 2>/dev/null || echo invalid)
  case "$node_supported" in
    ''|*[!0-9]*) err "could not determine the Node.js version from 'node'." ;;
  esac
  [ "$node_supported" -eq 1 ] || \
    err "Node.js >= ${MIN_NODE_VERSION} 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

  # Probe both write locations before downloading so failures name the blocked
  # path instead of making a blanket sudo rerun look necessary.
  ensure_writable_install_dir() {
    install_dir=$1
    if ! mkdir -p "$install_dir" 2>/dev/null; then
      err "cannot create the Qodo install directory '$install_dir'.
  This installer is user-scoped; do not rerun it with sudo.
  Check the ownership and permissions of this exact path, then rerun as your normal user."
    fi

    write_probe=$(node -e '
      const { mkdtempSync } = require("node:fs");
      process.stdout.write(mkdtempSync(process.argv[1] + "/.qodo-write-test."));
    ' "$install_dir" 2>/dev/null) || \
      err "the Qodo install directory '$install_dir' is not writable.
  It may belong to another user after an earlier elevated install.
  Do not rerun this installer with sudo. Correct this directory's ownership or permissions, then rerun as your normal user."
    rmdir "$write_probe" 2>/dev/null || \
      err "could not clean up the write check in '$install_dir'."
  }

  ensure_writable_install_dir "$QODO_HOME"
  ensure_writable_install_dir "$BIN_DIR"

  # --- 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

  # `npm rm -g` only cleans the ACTIVE npm prefix. It misses the old `qodo` when
  # npm is absent from this shell (for example a GUI-launched minimal PATH), when
  # @qodo/command lives under a DIFFERENT prefix (nvm/asdf switch, system vs brew
  # node), or when the package is gone but its bin was left behind — in all three
  # the stale binary still sits ahead on PATH and shadows us. So also resolve the
  # on-PATH `qodo` directly and unlink it — but ONLY when it provably belongs to
  # @qodo/command (walk the symlink to its package.json, mirroring preinstall.mjs),
  # never an unattributable `qodo`. Skips $BIN_DIR/qodo so a reinstall/overlapping
  # prefix can't delete our own launcher. node is a hard dep (checked above).
  existing_qodo=$(command -v qodo 2>/dev/null || true)
  if [ -n "$existing_qodo" ] && [ "$existing_qodo" != "$BIN_DIR/qodo" ]; then
    stale=$(node -e '
      const { realpathSync, existsSync, readFileSync } = require("fs");
      const { dirname, join } = require("path");
      const bin = process.argv[1];
      let real; try { real = realpathSync(bin); } catch { process.exit(0); }
      let dir = dirname(real);
      for (let i = 0; i < 8; i++) {
        const pj = join(dir, "package.json");
        if (existsSync(pj)) {
          try {
            if (JSON.parse(readFileSync(pj, "utf8")).name === "@qodo/command") {
              process.stdout.write(bin); // the PATH entry to unlink
            }
            break;
          } catch { /* keep walking */ }
        }
        const up = dirname(dir);
        if (up === dir) break;
        dir = up;
      }
    ' "$existing_qodo" 2>/dev/null) || stale=""
    if [ -n "$stale" ]; then
      if rm -f "$stale" 2>/dev/null; then
        echo "qodo install: removed the deprecated @qodo/command binary at $stale" >&2
      else
        echo "qodo install: couldn't remove the old qodo at $stale — delete it manually." >&2
      fi
    fi
  fi

  # --- download + verify ----------------------------------------------------
  # 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

  # Distribution metadata is independent of login state and survives logout.
  # Persist it before swapping the binary: if this fails, the old binary/origin
  # pair remains intact instead of installing private bytes with a public updater.
  update_base_tmp="$QODO_HOME/.update-base-url.$$"
  printf '%s\n' "$BASE" > "$update_base_tmp"
  mv -f "$update_base_tmp" "$QODO_HOME/update-base-url"

  # --- 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"

  # --- immediate access symlink --------------------------------------------
  # An installer child process cannot export PATH into the parent shell. If the
  # user already has a writable HOME-owned bin dir on the live PATH (prefer
  # ~/.local/bin), link our canonical shim there so `qodo` resolves immediately.
  # This is best-effort only: never write outside HOME, never clobber a foreign
  # qodo, and never fail the install if symlinks are unavailable.
  bin_dir_on_path=0
  case ":${PATH:-}:" in
    *":$BIN_DIR:"*|*":$BIN_DIR/:"*) bin_dir_on_path=1 ;;
  esac
  immediate_link=""
  immediate_link_dir=""
  immediate_blocked=""
  immediate_ready=0

  # Print the immediate (single-hop) target of a symlink, or nothing. Never
  # fails — a non-symlink or a broken readlink yields an empty string, which
  # every caller treats as "not one of ours".
  symlink_target() {
    readlink "$1" 2>/dev/null || true
  }

  # True when $1 is a `qodo` that this installer owns: the canonical shim
  # itself, the immediate symlink we just made, or any symlink pointing at the
  # shim. Used to distinguish our own install from a foreign `qodo` on PATH so
  # the shadow warning below doesn't fire on us. We only ever create single-hop
  # links to $BIN_DIR/qodo, so a one-level readlink is sufficient here.
  is_our_qodo_path() {
    qodo_path=$1
    [ "$qodo_path" = "$BIN_DIR/qodo" ] && return 0
    [ -n "$immediate_link" ] && [ "$qodo_path" = "$immediate_link" ] && return 0
    if [ -L "$qodo_path" ]; then
      qodo_target=$(symlink_target "$qodo_path")
      [ "$qodo_target" = "$BIN_DIR/qodo" ] && return 0
    fi
    return 1
  }

  # Try to place our immediate `qodo` symlink in $1. TRI-STATE return, because
  # the caller's loop must tell "keep looking" apart from "stop, we're done":
  #   0  linked (or refreshed our own prior link) — sets immediate_link[_dir]
  #   1  not usable here (missing/not writable/escapes HOME) — keep looking
  #   2  a runnable FOREIGN qodo occupies this slot — sets immediate_blocked,
  #      stop looking
  # rc 2 is terminal because a runnable foreign qodo in a HOME dir sits at a
  # fixed spot on PATH; anything we could link into a later dir would just be
  # shadowed by it, so there is no point continuing — we surface it as a shadow
  # warning. Non-runnable occupants are not clobbered, but shells skip them
  # during PATH lookup, so they are treated as "keep looking" instead.
  # The HOME-containment guard is deliberately belt-and-suspenders: a
  # lexical prefix check AND a physical `pwd -P` check, so a symlinked PATH entry
  # can't trick us into writing outside the user's home.
  try_immediate_link_dir() {
    candidate_dir=$1
    [ -n "$candidate_dir" ] || return 1
    [ -d "$candidate_dir" ] || return 1
    [ -w "$candidate_dir" ] || return 1
    [ -n "${HOME:-}" ] || return 1
    home_dir=${HOME%/}
    [ -n "$home_dir" ] && [ "$home_dir" != "/" ] || return 1
    case "$candidate_dir" in
      "$home_dir"/*) ;;
      *) return 1 ;;
    esac
    physical_home=$(cd "$home_dir" 2>/dev/null && pwd -P) || return 1
    physical_dir=$(cd "$candidate_dir" 2>/dev/null && pwd -P) || return 1
    [ -n "$physical_home" ] && [ "$physical_home" != "/" ] || return 1
    case "$physical_dir" in
      "$physical_home"/*) ;;
      *) return 1 ;;
    esac

    link_path="$candidate_dir/qodo"
    if [ -e "$link_path" ] || [ -L "$link_path" ]; then
      if [ -L "$link_path" ]; then
        link_target=$(symlink_target "$link_path")
        case "$link_target" in
          "$BIN_DIR"/*) ;;
          *)
            if [ -f "$link_path" ] && [ -x "$link_path" ]; then
              immediate_blocked="$link_path"
              return 2
            fi
            return 1
            ;;
        esac
      elif [ -f "$link_path" ] && [ -x "$link_path" ]; then
        immediate_blocked="$link_path"
        return 2
      else
        return 1
      fi
    fi

    # If this is a previous link of ours, remove the link itself before
    # recreating it so ln never follows a symlink-to-directory edge case.
    if [ -L "$link_path" ]; then
      rm -f "$link_path" || return 1
    fi
    if ln -sf "$BIN_DIR/qodo" "$link_path" 2>/dev/null; then
      immediate_link="$link_path"
      immediate_link_dir="$candidate_dir"
      return 0
    fi
    return 1
  }

  # Wrap try_immediate_link_dir so a loop can branch on "keep going" vs "stop".
  # Returns 0 when the search should STOP (we linked, or hit a runnable foreign qodo),
  # 1 when the caller should try the next candidate. The rc must be captured
  # inside the `else` branch: after a not-taken `if` with no else, $? is reset
  # to 0, which would silently swallow the terminal rc=2.
  consider_link_dir() {
    if try_immediate_link_dir "$1"; then
      return 0            # linked ours — done
    else
      [ "$?" -eq 2 ] && return 0   # runnable foreign qodo here — done, warn later
      return 1                     # unusable — keep looking
    fi
  }

  if [ "$bin_dir_on_path" != "1" ] && [ -n "${HOME:-}" ]; then
    immediate_done=0
    home_dir=${HOME%/}
    if [ -n "$home_dir" ] && [ "$home_dir" != "/" ]; then
      # Prefer ~/.local/bin (the XDG-ish user bin dir) when it's on PATH, then
      # fall back to the first other HOME-owned writable dir in PATH order.
      local_bin="$home_dir/.local/bin"
      case ":${PATH:-}:" in
        *":$local_bin:"*|*":$local_bin/:"*)
          consider_link_dir "$local_bin" && immediate_done=1 ;;
      esac

      if [ "$immediate_done" != "1" ]; then
        # Feed the loop from a heredoc, not a pipe: a piped `while` runs in a
        # subshell and would lose the immediate_link/immediate_blocked writes.
        path_entries=$(printf '%s' "${PATH:-}" | tr ':' '\n')
        while IFS= read -r candidate_dir; do
          [ -n "$candidate_dir" ] || continue
          case "$candidate_dir" in
            "$local_bin"|"$local_bin/") continue ;;   # already tried above
          esac
          if consider_link_dir "$candidate_dir"; then
            immediate_done=1
            break
          fi
        done <<QODO_PATH_ENTRIES
$path_entries
QODO_PATH_ENTRIES
      fi
    fi

    if [ -n "$immediate_link" ]; then
      resolved=$(command -v qodo 2>/dev/null || true)
      if [ -n "$resolved" ] && is_our_qodo_path "$resolved"; then
        immediate_ready=1
      fi
    fi
  fi

  # --- PATH wiring ----------------------------------------------------------
  # Keep the canonical shim directory wired into shell startup files even when
  # the immediate symlink above makes `qodo` work in this already-running shell.
  # Only repeat the reload hint at the end when no immediate access is available
  # and startup-file wiring was actually confirmed.
  reload_hint=""
  path_persisted=0
  case ":${PATH:-}:" in
    *":$BIN_DIR:"*|*":$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:\"*|*\":$BIN_DIR/:\"*) ;; *) export PATH=\"$BIN_DIR:\$PATH\" ;; esac"
      # Idempotent append — don't duplicate the line on re-install.
      profile_write_failed=0
      wire_rc() {
        if [ ! -f "$1" ] || ! grep -qF "$BIN_DIR" "$1"; then
          if (printf '\n# Added by the Qodo CLI installer\n%s\n' "$line" >> "$1") 2>/dev/null; then
            echo "qodo install: added $BIN_DIR to PATH in $1" >&2
          else
            # The CLI is already installed. Shell-profile persistence is an
            # optional convenience, so surface the exact file and keep going.
            profile_write_failed=1
            echo "qodo install: warning — couldn't add $BIN_DIR to PATH in '$1' (not writable)." >&2
            return 0
          fi
        else
          echo "qodo install: PATH already wired in $1" >&2
        fi
        path_persisted=1
        if [ "$immediate_ready" != "1" ]; then
          reload_hint="$hint"
        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"
            ;;
          *)
            if [ "$immediate_ready" != "1" ]; then
              echo "qodo install: add $BIN_DIR to your PATH:" >&2
              echo "  $hint" >&2
            fi
            ;;
        esac
        if [ "$profile_write_failed" = "1" ] && [ "$path_persisted" != "1" ]; then
          echo "qodo install: add $BIN_DIR to PATH manually:" >&2
          echo "  $hint" >&2
        fi
      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" ] && ! is_our_qodo_path "$existing"; 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
  elif [ -n "$immediate_blocked" ]; then
    echo "qodo install: warning — another 'qodo' is ahead on PATH: $immediate_blocked" >&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

  # The installed CLI is not always this installer's contemporary — an older
  # channel pin, or a deployment-local image shipping a fixed qodo — and passing a
  # flag it doesn't know makes setup exit non-zero, which would leave a printed
  # `qodo login --auth-url …` fallback that fails the same way. So ask the binary
  # what it supports (offline, no network) and use the spelling it accepts:
  # `--auth-url` on current builds, the older `--platform-url` on `login` before
  # that. A build with neither can't be pointed anywhere, which the user must know.
  auth_flag=""
  if [ -n "$auth_url" ]; then
    if "$BIN_DIR/qodo" setup --help 2>/dev/null | grep -q -- '--auth-url'; then
      auth_flag="--auth-url"
    elif "$BIN_DIR/qodo" login --help 2>/dev/null | grep -q -- '--platform-url'; then
      auth_flag="--platform-url"
      echo "qodo install: this qodo predates --auth-url; use --platform-url for login." >&2
    else
      echo "qodo install: warning — this qodo cannot be pointed at $auth_url. Update it, then re-run login." >&2
    fi
  fi

  # Two invocations rather than an eval'd/word-split argument string: POSIX sh has
  # no arrays, and the URL must reach the CLI as one argv entry.
  run_setup() {
    if [ -n "$auth_url" ]; then
      "$BIN_DIR/qodo" setup --auth-url "$auth_url" < /dev/tty
    else
      "$BIN_DIR/qodo" setup < /dev/tty
    fi
  }

  # 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: use /dev/tty so redirected/non-interactive stdin never
  # captures prompts. 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.
  # An endpoint was asked for but this build's `setup` can't carry it. Running setup
  # anyway would offer a login that authenticates against CLOUD instead of
  # "$auth_url" — and it would exit 0, so the corrected login line below would never
  # print. A silent wrong-platform login is worse than a manual step, so skip the
  # automatic setup here and let the printed steps carry the right command.
  setup_ok=0
  if [ -n "$auth_url" ] && [ "$auth_flag" != "--auth-url" ]; then
    echo "qodo install: skipping automatic setup — this qodo can't sign in to $auth_url." >&2
  elif [ -t 1 ] && [ -r /dev/tty ]; then
    printf '\n' >&2
    if run_setup; 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.
  # The login line carries the auth URL when one was given: plain `qodo login`
  # would go to cloud, which for a deployment-local install is the wrong platform.
  if [ "$setup_ok" != "1" ]; then
    if [ -n "$auth_flag" ]; then
      login_line="qodo login $auth_flag $auth_url   # connect your Qodo account"
    elif [ -n "$auth_url" ]; then
      # Deliberately NOT a bare `qodo login`: on this build that authenticates
      # against cloud, not "$auth_url" — the failure this whole path guards.
      login_line="# this qodo can't sign in to $auth_url — update it (https://get.qodo.ai), then: qodo login --auth-url $auth_url"
    else
      login_line="qodo login   # connect your Qodo account"
    fi
    printf '\n  Finish setup:\n\n    qodo skills install     # add skills to your coding agents\n    %s\n' "$login_line" >&2
  fi

  # LAST line of output, so it can't scroll off behind the banner/setup/examples
  # above. When we added/found $BIN_DIR in an rc file, THIS shell won't pick it
  # up until it re-reads that rc — a child process can't mutate its parent shell's
  # PATH. If no rc file was wired (e.g. HOME unset / unsupported shell), the
  # manual instruction above is the only safe claim to make.
  if [ "$path_persisted" = "1" ] && [ -n "$reload_hint" ]; then
    printf "\n  ⚠  \`qodo\` is on your PATH for NEW shells only. To use it in THIS shell now:\n\n    %s\n\n  (or open a new terminal / run: exec %s)\n" \
      "$reload_hint" "$(basename "${SHELL:-sh}")" >&2
  elif [ "$immediate_ready" = "1" ]; then
    printf "\n  qodo install: \`qodo\` is ready to use now (linked into %s).\n  qodo install: if \`qodo\` is not found, run \`hash -r\` (bash/zsh) or open a new terminal.\n" \
      "$immediate_link_dir" >&2
  fi
}

main "$@"
