#!/usr/bin/env bash
# Stele CLI installer.
#
#   curl -fsSL https://app.stele-ai.dev/install.sh | bash
#
# Downloads a standalone, self-contained `stele` binary (no Node required) from
# Supabase Storage and drops it in ~/.local/bin. Binaries are built with
# `bun build --compile` and shipped as gzip archives (see
# apps/cli/scripts/build-binaries.mjs).

set -uo pipefail

# ── Config ───────────────────────────────────────────────────────────────
STORAGE_BASE="${STELE_INSTALL_BASE:-https://gptpfohfoskqngobigfb.supabase.co/storage/v1/object/public/cli}"
INSTALL_DIR="${STELE_INSTALL_DIR:-$HOME/.local/bin}"
# Strip a trailing slash so the "$INSTALL_DIR/stele" paths built below compare
# cleanly against what `command -v` reports. ("/" itself is left alone — the
# strip would empty it.)
[[ "$INSTALL_DIR" != "/" ]] && INSTALL_DIR="${INSTALL_DIR%/}"
BINARY_NAME="stele"

requested_version="${STELE_VERSION:-latest}"
no_modify_path=false
force_install=false
# Remembered mode: after placing the binary, run `stele install --remembered`
# (non-interactive refresh of the user's saved harness selection) instead of the
# interactive wizard. `stele update` pipes this so an update never re-opens the
# picker. See apps/cli/src/cmds/update.ts.
remembered=false
# Binary-only mode: place the binary and stop — skip the `stele install` step
# (Claude Code plugin / marketplace setup). The desktop app uses this: it
# installs the binary, then drives plugin install itself per chosen agent.
skip_plugin_install=false
[[ -n "${STELE_NO_PLUGIN_INSTALL:-}" ]] && skip_plugin_install=true

# Populated by configure_path so main() can print a precise "make it live" hint
# after the install: CONFIGURED_RC is the shell rc that now carries the PATH
# line (sourcing it activates `stele` in the current shell); PATH_EXPORT_CMD is
# the raw command to run when we couldn't write any rc (e.g. --no-modify-path).
CONFIGURED_RC=""
PATH_EXPORT_CMD=""

# ── Colors ───────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
    DIM='\033[2m'; CYAN='\033[0;36m'; GREEN='\033[0;32m'
    RED='\033[0;31m'; YELLOW='\033[0;33m'; BOLD='\033[1m'; NC='\033[0m'
else
    DIM=''; CYAN=''; GREEN=''; RED=''; YELLOW=''; BOLD=''; NC=''
fi

usage() {
    cat <<EOF
Stele installer

Usage: install.sh [options]

Options:
    -h, --help              Show this help
    -v, --version <ver>     Install a specific version (e.g. 0.1.0)
    -f, --force             Reinstall even if the same version is present
        --no-modify-path    Don't touch shell config files
        --no-plugin-install Only place the binary; skip 'stele install'
        --remembered        Refresh the saved harness selection, no wizard (used by 'stele update')

Environment:
    STELE_INSTALL_DIR       Install location (default: ~/.local/bin)
    STELE_INSTALL_BASE      Storage base URL (for staging)
    STELE_NO_PLUGIN_INSTALL Set (to any value) to skip plugin install (binary only)

Examples:
    curl -fsSL https://app.stele-ai.dev/install.sh | bash
    curl -fsSL https://app.stele-ai.dev/install.sh | bash -s -- -v 0.1.0
EOF
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help) usage; exit 0 ;;
        -v|--version)
            [[ -n "${2:-}" ]] || { echo "Error: --version needs an argument"; exit 1; }
            requested_version="$2"; shift 2 ;;
        -f|--force) force_install=true; shift ;;
        --no-modify-path) no_modify_path=true; shift ;;
        --no-plugin-install|--binary-only) skip_plugin_install=true; shift ;;
        --remembered) remembered=true; shift ;;
        *) echo -e "${YELLOW}Unknown option: $1${NC}" >&2; shift ;;
    esac
done

say()   { echo -e "$1"; }
info()  { echo -e "${DIM}$1${NC}"; }
ok()    { echo -e "${GREEN}$1${NC}"; }
warn()  { echo -e "${YELLOW}$1${NC}"; }
die()   { echo -e "${RED}$1${NC}" >&2; exit 1; }

# ── Banner ───────────────────────────────────────────────────────────────
# The Stele wordmark. MUST match apps/cli/src/lib/tty.ts (STELE_LOGO_LINES /
# logo()) so the `curl | bash` → install-wizard handoff reads as one continuous
# brand. If you change the art, change it in BOTH places.
banner() {
    echo ""
    echo -e "  ${CYAN}╭─╮╶┬╴╭─╴╷  ╭─╴   ╭─╮╷${NC}"
    echo -e "  ${CYAN}╰─╮ │ ├╴ │  ├╴    ├─┤│${NC}"
    echo -e "  ${CYAN}╰─╯ ╵ ╰─╴╰─╴╰─╴   ╵ ╵╵${NC}"
    echo -e "  ${DIM}the agentic ledger${NC}"
    echo ""
}

# ── Detect platform ──────────────────────────────────────────────────────
detect_asset() {
    local os arch
    case "$(uname -s)" in
        Darwin*) os="darwin" ;;
        Linux*)  os="linux" ;;
        MINGW*|MSYS*|CYGWIN*)
            die "Windows: download stele-windows-x64.exe.tar.gz from the releases page, or use WSL." ;;
        *) die "Unsupported OS: $(uname -s)" ;;
    esac

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

    # Rosetta: an x86_64 process on Apple Silicon — prefer the native arm64 build.
    if [[ "$os" == "darwin" && "$arch" == "x64" ]]; then
        if [[ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" == "1" ]]; then
            arch="arm64"
        fi
    fi

    echo "${BINARY_NAME}-${os}-${arch}"
}

# ── Resolve version ──────────────────────────────────────────────────────
resolve_version() {
    if [[ "$requested_version" != "latest" ]]; then
        echo "${requested_version#v}"
        return
    fi
    local json
    json="$(curl -fsSL "${STORAGE_BASE}/latest.json" 2>/dev/null)" \
        || die "Could not reach the release index (${STORAGE_BASE}/latest.json)"
    echo "$json" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' \
        | head -1 | sed -E 's/.*"([^"]+)"$/\1/'
}

# ── Progress-bar download (falls back to plain on non-TTY) ───────────────
# Uses █/░ in the accent color so the download bar matches the per-harness
# install bars (apps/cli/src/lib/tty.ts progressBar). Built char-by-char rather
# than via `tr` — the block glyphs are multibyte and `tr` mangles them.
draw_bar() {
    local pct=$1 width=28 i filled bar=""
    [[ $pct -gt 100 ]] && pct=100
    filled=$((pct * width / 100))
    for ((i = 0; i < width; i++)); do
        if (( i < filled )); then bar+="█"; else bar+="░"; fi
    done
    printf "\r  ${CYAN}%s${NC} %3d%%" "$bar" "$pct" >&2
}

download() {
    local url=$1 out=$2
    if [[ ! -t 2 ]]; then
        curl -fsSL "$url" -o "$out"; return $?
    fi
    local total
    total="$(curl -sIL "$url" | grep -i '^content-length:' | tail -1 | awk '{print $2}' | tr -d '\r')"
    if [[ -z "$total" || "$total" -lt 100000 ]]; then
        curl -fsSL "$url" -o "$out"; return $?
    fi
    printf '\033[?25l' >&2   # hide cursor
    curl -fsSL "$url" -o "$out" &
    local pid=$!
    sleep 0.1
    while kill -0 "$pid" 2>/dev/null; do
        if [[ -f "$out" ]]; then
            local cur
            cur="$(stat -f%z "$out" 2>/dev/null || stat -c%s "$out" 2>/dev/null || echo 0)"
            [[ "$cur" -gt 0 ]] && draw_bar $((cur * 100 / total))
        fi
        sleep 0.1
    done
    wait "$pid"; local status=$?
    draw_bar 100; echo "" >&2
    printf '\033[?25h' >&2   # show cursor
    return $status
}

# ── Path canonicalization ────────────────────────────────────────────────
# Normalizes a path for comparison: resolves symlinked/relative parent dirs and
# collapses `//`, `.`, `..`. Deliberately does NOT resolve a symlinked leaf —
# the question is which file will run, not what it ultimately points at.
#
# Hand-rolled because `realpath` isn't guaranteed on macOS; `dirname`/`basename`
# and `pwd -P` are POSIX and present everywhere.
canonical_path() {
    local p=$1 dir
    [[ -n "$p" ]] || return 0
    dir="$(cd "$(dirname "$p")" 2>/dev/null && pwd -P)" || { echo "$p"; return; }
    echo "${dir%/}/$(basename "$p")"
}

# ── sha256 verify ────────────────────────────────────────────────────────
sha256_of() {
    if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
    elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}';
    else echo ""; fi
}

# ── PATH config ──────────────────────────────────────────────────────────
configure_path() {
    # A sensible manual fallback the final message can always fall back to.
    PATH_EXPORT_CMD="export PATH=\"$INSTALL_DIR:\$PATH\""

    [[ "$no_modify_path" == "true" ]] && return
    # Never persist a temp-dir install into shell config — temp dirs get
    # reaped, leaving dead PATH entries behind (smoke tests bit us here).
    case "$INSTALL_DIR" in /tmp/*) return ;; esac
    if [[ -n "${TMPDIR:-}" && "${TMPDIR%/}" != "" ]]; then
        case "$INSTALL_DIR" in "${TMPDIR%/}"/*) return ;; esac
    fi
    case ":$PATH:" in *":$INSTALL_DIR:"*) return ;; esac

    local shell_name config_file path_cmd
    shell_name="$(basename "${SHELL:-}")"
    case "$shell_name" in
        zsh)  config_file="${ZDOTDIR:-$HOME}/.zshrc"; path_cmd="export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
        bash) config_file="$HOME/.bashrc"; [[ -f "$config_file" ]] || config_file="$HOME/.bash_profile"
              path_cmd="export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
        fish) config_file="$HOME/.config/fish/config.fish"; path_cmd="fish_add_path $INSTALL_DIR" ;;
        *)    config_file="$HOME/.profile"; path_cmd="export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
    esac
    PATH_EXPORT_CMD="$path_cmd"

    # Already wired up in this rc — sourcing it still activates stele in the
    # current shell, so remember it for the final "make it live" hint.
    if [[ -f "$config_file" ]] && grep -Fq "$INSTALL_DIR" "$config_file" 2>/dev/null; then
        CONFIGURED_RC="$config_file"
        return
    fi
    if {
        echo ""
        echo "# stele"
        echo "$path_cmd"
    } >> "$config_file" 2>/dev/null; then
        CONFIGURED_RC="$config_file"
        info "Added $INSTALL_DIR to PATH in $config_file"
    else
        warn "Couldn't update your shell config automatically — see below to finish."
    fi

    [[ "${GITHUB_ACTIONS:-}" == "true" && -n "${GITHUB_PATH:-}" ]] && echo "$INSTALL_DIR" >> "$GITHUB_PATH"
}

# ── Main ─────────────────────────────────────────────────────────────────
main() {
    banner
    command -v curl >/dev/null 2>&1 || die "curl is required."

    local asset version
    asset="$(detect_asset)"
    version="$(resolve_version)"
    [[ -n "$version" ]] || die "Could not determine version to install."

    # Already installed at this version?
    #
    # Ask about the file we are about to write — NOT `command -v stele`.
    # STELE_INSTALL_DIR chooses where the binary goes, so it must also decide
    # what counts as "already there"; a PATH lookup answers a different question
    # and finds some *other* copy. That mismatch let the installer report
    # success while writing nothing to the directory the user asked for, and it
    # made `pnpm release:verify --install` fail on every machine that already
    # had stele on PATH.
    #
    # Consequence, accepted deliberately: a user with stele elsewhere on PATH
    # who re-runs this without --force now gets a copy in INSTALL_DIR too. That
    # is the honest outcome — we install where we were told to — and the
    # shadowing hint at the end of main() names the other copy so the duplicate
    # is surfaced rather than hidden.
    local installed="$INSTALL_DIR/$BINARY_NAME"
    if [[ "$force_install" != "true" && -x "$installed" ]]; then
        local cur; cur="$("$installed" --version 2>/dev/null | tr -d '[:space:]')"
        if [[ "$cur" == "$version" ]]; then
            ok "✓ stele $version is already installed in $INSTALL_DIR (use --force to reinstall)."
            exit 0
        fi
        info "Upgrading stele ${cur:-?} → $version"
    fi

    # Binaries ship as gzip archives — the extracted file is the original,
    # unmodified Mach-O/ELF/PE (UPX would break code-signing on Apple Silicon).
    local base="${STORAGE_BASE}/v${version}"
    local archive="${asset}.tar.gz"
    local tmp; tmp="$(mktemp -d)"
    # Expand $tmp now (double quotes): the EXIT trap runs in global scope where
    # this function-local would otherwise be unset under `set -u`.
    trap "rm -rf '$tmp'" EXIT
    local out="$tmp/$archive"

    info "Downloading stele $version ($asset)…"
    download "${base}/${archive}" "$out" || die "Download failed: ${base}/${archive}"

    # Verify checksum of the downloaded archive when the sidecar is present.
    local want got
    want="$(curl -fsSL "${base}/${archive}.sha256" 2>/dev/null | awk '{print $1}')"
    if [[ -n "$want" ]]; then
        got="$(sha256_of "$out")"
        if [[ -n "$got" && "$got" != "$want" ]]; then
            die "Checksum mismatch — refusing to install.\n  expected $want\n  got      $got"
        fi
        [[ -n "$got" ]] && info "Checksum verified."
    else
        warn "No checksum published for $archive — skipping verification."
    fi

    # Extract: the archive holds the bare binary named "$asset".
    tar -xzf "$out" -C "$tmp" || die "Failed to extract $archive"
    [[ -f "$tmp/$asset" ]] || die "Archive did not contain expected binary: $asset"

    mkdir -p "$INSTALL_DIR"
    chmod +x "$tmp/$asset"
    mv -f "$tmp/$asset" "$INSTALL_DIR/$BINARY_NAME"

    configure_path

    local stele_bin="$INSTALL_DIR/$BINARY_NAME"
    echo ""
    ok "✓ Installed stele $version → $stele_bin"

    # Wire up / refresh the Stele plugin (synthesizes the marketplace and
    # registers hooks + MCP). Critical on upgrades: a new binary ships new hook
    # and MCP behavior that the previously-installed plugin scaffold won't have
    # until this runs. Call by absolute path — the updated PATH isn't active in
    # this shell yet. Non-fatal: the CLI is usable even if this step can't
    # complete (e.g. no supported agent is installed).
    #
    # STELE_INSTALL_SH=1 tells the CLI it was launched from here, so the wizard
    # doesn't re-print the logo we already showed, and the CLI's outro owns the
    # getting-started footer on the interactive path (we stay quiet there).
    #
    # Three modes:
    #   --remembered   → `stele install --remembered` — non-interactive refresh
    #                    of the saved harness selection (`stele update` uses this;
    #                    never the wizard).
    #   a terminal     → `stele install </dev/tty` — the interactive picker even
    #                    under `curl | bash` (our stdin is the download pipe, so we
    #                    hand the CLI the real controlling terminal).
    #   no tty / CI    → `stele install --yes` — install every detected agent, no
    #                    prompt.
    #
    # Binary-only mode skips all of this: the desktop app installs the binary,
    # then drives plugin install itself (per chosen agent) from its onboarding.
    local wizard_ran=false
    if [[ "$skip_plugin_install" == "true" ]]; then
        info "Binary-only install — skipping plugin setup ('stele install')."
    elif [[ "$remembered" == "true" ]]; then
        echo ""
        info "Refreshing the Stele plugin…"
        STELE_INSTALL_SH=1 "$stele_bin" install --remembered || \
            warn "  Plugin refresh didn't complete — run 'stele install' to set up your agents."
    elif [[ -r /dev/tty ]]; then
        echo ""
        if STELE_INSTALL_SH=1 "$stele_bin" install < /dev/tty; then
            wizard_ran=true
        else
            warn "  Setup didn't complete — run 'stele install' any time."
        fi
    else
        echo ""
        info "Setting up the Stele plugin…"
        STELE_INSTALL_SH=1 "$stele_bin" install --yes || \
            warn "  'stele install' didn't complete (is a supported agent installed?). Run 'stele install' later."
    fi

    # Getting-started footer — printed ONLY when the wizard did NOT run and this
    # isn't an update refresh. The wizard's own outro owns it on the interactive
    # path; an update doesn't need it. This is the fresh no-tty / binary-only case.
    #
    # Deliberately says nothing about who may sign up. The access model flips
    # without a deploy, and this script is fetched fresh on every install — a
    # hardcoded "invite-only beta" line went stale the moment signup opened.
    # Sign-in itself reports the truth when someone is actually turned away.
    if [[ "$wizard_ran" != "true" && "$remembered" != "true" ]]; then
        echo ""
        echo -e "  ${DIM}Get started:${NC}"
        echo -e "    stele account login   ${DIM}# sign in${NC}"
        echo -e "    stele web             ${DIM}# open the dashboard${NC}"
        echo ""
    fi
    # Does `stele` resolve to what we just installed? Three outcomes, and a bare
    # `command -v` could only tell the first from the other two:
    #
    #   it resolves here      → nothing to say.
    #   it resolves nowhere   → PATH isn't live in this shell yet. Expected on a
    #                           fresh install (nothing failed), so say it calmly
    #                           and give the exact one-liner to fix it.
    #   it resolves elsewhere → another stele shadows the one we just installed.
    #                           Silence here is the worst outcome: the user runs
    #                           `stele`, gets the old binary, and every symptom
    #                           points at an install that "succeeded".
    local resolved; resolved="$(canonical_path "$(command -v "$BINARY_NAME" 2>/dev/null)")"
    if [[ "$resolved" != "$(canonical_path "$stele_bin")" ]]; then
        echo ""
        if [[ -n "$resolved" ]]; then
            warn "Another stele comes before $INSTALL_DIR on your PATH:"
            say  "    $resolved"
            info "Remove it, or put $INSTALL_DIR first, to run the version you just installed."
        elif [[ -n "$CONFIGURED_RC" ]]; then
            info "stele isn't on your PATH in this shell yet. Open a new terminal, or run:"
            say "    source $CONFIGURED_RC"
        else
            info "stele isn't on your PATH yet. Add it with:"
            say "    ${PATH_EXPORT_CMD:-export PATH=\"$INSTALL_DIR:\$PATH\"}"
        fi
    fi
}

main
