#!/usr/bin/env sh
# SPDX-License-Identifier: BUSL-1.1
# Copyright (c) 2026 Eigentic, Inc. — see LICENSE.
#
# RookOne CLI one-line installer.
#
# Usage:
#     curl -LsSf https://get.rookone.io | sh
#   — or —
#     sh get/install.sh [--purge | --purge-identity | --help]
#
# The script:
#   1. Bootstraps uv (https://astral.sh/uv) if not already on PATH.
#   2. Installs the `rookone` CLI via `uv tool install` against the
#      RookOne package index; PyPI is the fallback for all other deps.
#   3. Prints next steps.
#
# IMPORTANT: execute this script, do NOT source it.
#   Run:   sh get/install.sh       (or via curl | sh)
#   NOT:   . get/install.sh        (set -eu would bleed into your shell)
#
# rookone-channel (the Claude Code MCP plugin) is NOT installed by default
# because it requires Claude Code to be useful.  See the "Optional" note
# printed after install.
#
# Environment overrides:
#   ROOKONE_INDEX_URL   PEP 503 index URL for rookone-* wheels.
#                       Default: https://packages.rookone.io/simple
#                       Set to a local URL when testing before the real
#                       index is live.

set -eu

ROOKONE_INDEX_URL="${ROOKONE_INDEX_URL:-https://packages.rookone.io/simple}"

# Temporary file for the uv bootstrap download; cleaned up by the EXIT trap.
_rk_uv_tmp=""

_rk_cleanup() {
    if [ -n "$_rk_uv_tmp" ]; then
        rm -f "$_rk_uv_tmp"
    fi
}
trap _rk_cleanup EXIT

# ---------------------------------------------------------------------------
# Logging helpers
# ---------------------------------------------------------------------------

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

log_info()    { printf '%b==>%b %s\n' "$BLUE"   "$NC" "$1" >&2; }
log_success() { printf '%b==>%b %s\n' "$GREEN"  "$NC" "$1" >&2; }
log_warning() { printf '%b==>%b %s\n' "$YELLOW" "$NC" "$1" >&2; }
log_error()   { printf '%bError:%b %s\n' "$RED"  "$NC" "$1" >&2; }

# ---------------------------------------------------------------------------
# Integrity / validation helpers
# ---------------------------------------------------------------------------

# Portable SHA-256 of a file → bare hex digest (sha256sum on Linux, shasum on
# macOS). Echoes the digest on stdout; non-zero if no tool is available.
_rk_sha256() {
    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
        log_error "no sha256 tool found (need sha256sum or shasum)"
        return 1
    fi
}

# Verify FILE matches EXPECTED sha256; clear error + non-zero on mismatch.
_rk_verify_sha256() {
    _rk_f="$1"; _rk_want="$2"
    _rk_have=$(_rk_sha256 "$_rk_f") || return 1
    if [ "$_rk_have" != "$_rk_want" ]; then
        log_error "integrity check failed: expected $_rk_want, got $_rk_have"
        return 1
    fi
    return 0
}

# Enforce HTTPS on the package index URL. http:// is rejected (a downgrade lets
# a network/MITM attacker serve unsigned packages, and unsafe-best-match would
# pick a malicious high version), EXCEPT loopback, which the CI smoke uses.
_rk_require_https() {
    case "$1" in
        https://*) return 0 ;;
        http://localhost:*|http://localhost/*|http://127.0.0.1:*|http://127.0.0.1/*) return 0 ;;
        *) log_error "ROOKONE_INDEX_URL must use HTTPS (got: $1)"; return 1 ;;
    esac
}

# ---------------------------------------------------------------------------
# Bootstrap uv if not on PATH
# ---------------------------------------------------------------------------

ensure_uv() {
    if command -v uv >/dev/null 2>&1; then
        log_info "uv already installed ($(uv --version 2>/dev/null))"
        return
    fi

    log_info "uv not found — bootstrapping from astral.sh"

    if ! command -v curl >/dev/null 2>&1; then
        log_error "curl is required to bootstrap uv but was not found on PATH."
        log_error "Install curl first, then re-run the installer."
        exit 1
    fi

    # Download the uv installer to a temp file rather than piping into sh,
    # so that stdin (which may be this very script when invoked via
    # `curl ... | sh`) is not consumed by the inner shell process.
    _rk_uv_tmp=$(mktemp /tmp/uv-install.XXXXXX)

    # Pin the uv version and verify the installer's SHA-256 before executing it.
    # The unversioned https://astral.sh/uv/install.sh is always-latest and
    # unverified — a compromise of astral.sh (or a TLS-downgrade/DNS hijack)
    # would run attacker code as the user. The versioned installer below pins
    # APP_VERSION internally, so a verified script installs exactly this uv.
    #
    # To bump: set UV_VERSION, then refresh the hash with —
    #   curl -LsSf https://github.com/astral-sh/uv/releases/download/<ver>/uv-installer.sh | sha256sum
    UV_VERSION="0.11.19"
    UV_INSTALLER_SHA256="ef8cf0575d37cf3c72e05f153dd72a845a87a7bb9be86184d5fe931b8c426250"

    if ! curl -LsSf "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh" -o "$_rk_uv_tmp"; then
        log_error "Failed to download the pinned uv installer (v${UV_VERSION})."
        exit 1
    fi

    if ! _rk_verify_sha256 "$_rk_uv_tmp" "$UV_INSTALLER_SHA256"; then
        log_error "uv installer integrity check failed — refusing to execute. Aborting."
        exit 1
    fi
    log_info "uv installer v${UV_VERSION} verified (sha256 ok)"

    # Run the uv installer; redirect stdin so it cannot accidentally read
    # from the outer pipe when this script itself is streamed via curl|sh.
    sh "$_rk_uv_tmp" </dev/null
    rm -f "$_rk_uv_tmp"
    _rk_uv_tmp=""

    # The uv installer places the binary in ~/.local/bin by default on
    # Linux/macOS.  Add it to PATH for the remainder of this script.
    export PATH="$HOME/.local/bin:$PATH"

    if ! command -v uv >/dev/null 2>&1; then
        log_error "uv was installed but is not on PATH."
        log_error "Add ~/.local/bin to your PATH, then re-run the installer."
        exit 1
    fi

    log_success "uv installed ($(uv --version 2>/dev/null))"
}

# ---------------------------------------------------------------------------
# Existing-install detection + guardrails
# ---------------------------------------------------------------------------

# Echo the install situation as "CASE [DETAIL]" on stdout:
#   fresh              no prior rookone anywhere
#   uv <version>       rookone is a uv-managed tool (re-run / upgrade)
#   pipx <version>     rookone is a pipx-managed tool (transition → uv)
#   stale <path>       a rookone executable is on PATH but neither uv nor
#                      pipx owns it (e.g. an old `pip install --user`)
#
# Ownership is authoritative and checked FIRST (pipx, then uv); a bare PATH
# hit is only reported when no package manager claims the binary. pipx is
# checked before uv so a messy box with both resolves to the migration path.
detect_existing_tool() { # $1 = package/command name (== console-script name)
    _det_pkg="$1"
    if command -v pipx >/dev/null 2>&1; then
        _det_pipx_ver=$(pipx list --short 2>/dev/null | awk -v p="$_det_pkg" '$1==p{print $2; exit}')
        if [ -n "${_det_pipx_ver:-}" ]; then
            printf 'pipx %s\n' "$_det_pipx_ver"
            return 0
        fi
    fi

    if command -v uv >/dev/null 2>&1; then
        _det_uv_ver=$(uv tool list 2>/dev/null | awk -v p="$_det_pkg" '$1==p{print $2; exit}')
        if [ -n "${_det_uv_ver:-}" ]; then
            printf 'uv %s\n' "$_det_uv_ver"
            return 0
        fi
    fi

    _det_stale=$(command -v "$_det_pkg" 2>/dev/null || true)
    if [ -n "$_det_stale" ]; then
        printf 'stale %s\n' "$_det_stale"
        return 0
    fi

    printf 'fresh\n'
}

# Back-compat wrapper: rookone-specific detection used by the install-plan
# messaging and the test suite. Delegates to the generic detector.
detect_existing_rookone() {
    detect_existing_tool rookone
}

# LOUD reminder that ~/.rookone is the user's identity, printed whenever we
# touch an existing install. Yellow so it stands out from the normal flow.
print_identity_warning() {
    log_warning "Your RookOne identity lives in ~/.rookone (agent keys + local message DB)."
    log_warning "Upgrades do NOT touch it — your agent is preserved."
    log_warning "Do NOT 'rm -rf ~/.rookone' unless you want to start over as a brand-new agent."
}

# Print the per-case plan to the user (stderr). Pure messaging — no install.
announce_install_plan() {
    _rk_case="$1"
    _rk_detail="${2:-}"
    case "$_rk_case" in
        fresh)
            log_info "No existing rookone found — installing fresh."
            ;;
        uv)
            log_info "Existing rookone ${_rk_detail} found (uv tool). Upgrading in place to the latest."
            print_identity_warning
            ;;
        pipx)
            log_warning "Existing rookone ${_rk_detail} found, installed via pipx."
            log_warning "Migrating to uv tool (the canonical installer). Removing the pipx copy first…"
            print_identity_warning
            ;;
        stale)
            log_warning "Found an existing 'rookone' at ${_rk_detail} not managed by uv or pipx."
            log_warning "Installing a uv-managed copy. If the old one shadows it, remove it: rm ${_rk_detail}"
            print_identity_warning
            ;;
    esac
}

# ---------------------------------------------------------------------------
# Shared converge helper
# ---------------------------------------------------------------------------

# Converge a single uv-managed tool to the latest from the index. Force-
# reinstalls when ANY prior copy exists: a bare `uv tool install` NO-OPS on an
# already-present uv tool, so without --force a re-run silently keeps a
# stale/broken build (bd eigentic-communication-yl1r / dvij). Migrates a
# pipx-owned copy first (uv cannot reuse pipx's venv). The final command is
# `uv tool install`, so the function's exit status IS the install status: a
# caller using `if ! _rk_converge_tool X` gets non-fatal handling (set -e is
# suppressed in a condition), while a bare call stays fatal under set -e.
_rk_converge_tool() { # $1 = package/command name
    _ct_pkg="$1"
    _ct_plan=$(detect_existing_tool "$_ct_pkg")
    _ct_case=${_ct_plan%% *}

    # pipx -> uv migration: drop the pipx-owned copy first. pipx 1.12 refuses to
    # let uv reuse a venv it created in another session, so a bare uv install
    # over a pipx install hits a venv-reuse error.
    if [ "$_ct_case" = "pipx" ]; then
        pipx uninstall "$_ct_pkg" >/dev/null 2>&1 || true
    fi

    # --force whenever a prior copy exists (uv tool install no-ops on an
    # existing uv tool, and overwrites entry points from pipx/stale copies).
    _ct_force=""
    case "$_ct_case" in
        uv|pipx|stale) _ct_force="--force" ;;
    esac

    # shellcheck disable=SC2086  # _ct_force is intentionally word-split (empty or --force)
    uv tool install "$_ct_pkg" \
        --index "${ROOKONE_INDEX_URL}" \
        --index-strategy unsafe-best-match \
        $_ct_force
}

# ---------------------------------------------------------------------------
# Install rookone via uv tool
# ---------------------------------------------------------------------------

install_rookone() {
    _rk_plan=$(detect_existing_rookone)
    _rk_case=${_rk_plan%% *}
    _rk_detail=${_rk_plan#* }
    # No DETAIL field (the "fresh" case) → ${var#* } returns the word unchanged.
    [ "$_rk_detail" = "$_rk_plan" ] && _rk_detail=""

    announce_install_plan "$_rk_case" "$_rk_detail"

    log_info "Installing rookone from ${ROOKONE_INDEX_URL}"

    # _rk_converge_tool handles the pipx → uv migration, --force for an existing
    # install, and a plain fresh install. The bare (non-`if`) call is fatal
    # under set -e, which is correct: rookone is the primary tool and must
    # succeed for the install to be meaningful.
    _rk_converge_tool rookone

    # Ensure the uv tool bin dir is on PATH for this session so the
    # rookone command is immediately available after install.
    # Strip any stray newlines from the captured path before it enters PATH —
    # a buggy/compromised uv emitting a trailing newline would otherwise corrupt
    # PATH for the rest of the script. Empty (uv failed) → the documented default.
    _rk_tool_bin=$(uv tool dir --bin 2>/dev/null | tr -d '\n')
    [ -n "$_rk_tool_bin" ] || _rk_tool_bin="$HOME/.local/bin"
    export PATH="${_rk_tool_bin}:$PATH"
}

# ---------------------------------------------------------------------------
# Claude Code MCP registration
# ---------------------------------------------------------------------------
#
# Make `rookone` resolvable as an MCP server from ANY working directory, so
# `claude --dangerously-load-development-channels=server:rookone` works without
# a project-local `.mcp.json` in the cwd. We register at USER scope (writes
# ~/.claude.json), which Claude Code's MCP resolution — and the dev-channels
# flag — search regardless of the current directory.
#
# Only runs when Claude Code is detected; otherwise we leave a one-liner the
# user can run later. The registered command is `rookone-channel` (the MCP /
# push-channel server), which is always converged (force-reinstalled when a
# prior copy exists) so a re-run never leaves a stale/broken channel in place.
setup_claude_mcp() {
    if ! command -v claude >/dev/null 2>&1; then
        log_info "Claude Code not detected — skipping MCP setup."
        log_info "To enable it later, install Claude Code then re-run this installer"
        log_info "  (or run: claude mcp add --scope user rookone -- rookone-channel)."
        return 0
    fi

    # Always converge rookone-channel — force-reinstall when a prior copy is
    # present. The old `if ! command -v rookone-channel` guard SKIPPED an
    # already-present but stale/broken channel (e.g. a 0.3.x ZodError build),
    # so a re-run left the user broken. bd eigentic-communication-yl1r / dvij.
    log_info "Installing/updating rookone-channel (Claude Code MCP server)"
    if ! _rk_converge_tool rookone-channel; then
        log_warning "Could not install rookone-channel — skipping MCP registration."
        log_warning "  Run later: uv tool install rookone-channel --index \"${ROOKONE_INDEX_URL}\" --index-strategy unsafe-best-match"
        return 0
    fi
    # Ensure the freshly installed console script is visible on PATH below.
    _rk_tool_bin=$(uv tool dir --bin 2>/dev/null || printf '%s/.local/bin' "$HOME")
    export PATH="${_rk_tool_bin}:$PATH"

    # Register at user scope. `claude mcp add` exits non-zero if a server with
    # that name already exists at this scope — treat an existing registration
    # as success rather than clobbering a user's customised entry.
    if claude mcp add --scope user rookone -- rookone-channel >/dev/null 2>&1; then
        log_success "Registered the 'rookone' MCP server in Claude Code (user scope)."
        log_success "It resolves from any directory — launch with:"
        log_success "  claude --dangerously-load-development-channels=server:rookone"
    elif claude mcp get rookone >/dev/null 2>&1; then
        log_info "A 'rookone' MCP server is already registered in Claude Code — left as-is."
    else
        log_warning "Could not register the rookone MCP server automatically. Run:"
        log_warning "  claude mcp add --scope user rookone -- rookone-channel"
    fi
}

# ---------------------------------------------------------------------------
# Post-install instructions
# ---------------------------------------------------------------------------

print_next_steps() {
    printf '\n'
    log_success "rookone installed successfully!"
    printf '\n'
    printf 'Getting started:\n'
    printf '  rookone --help          show all commands\n'
    printf '  rookone register        create your agent\n'
    printf '  rookone whoami          confirm your identity\n'
    printf '  rookone inbox           check for new messages\n'
    printf '  rookone send            send a direct message\n'
    printf '\n'
    printf 'Claude Code (MCP):\n'
    if command -v claude >/dev/null 2>&1; then
        printf '  The "rookone" MCP server is registered (user scope) — works from any\n'
        printf '  directory. Start a session with real-time messaging:\n'
        printf '    claude --dangerously-load-development-channels=server:rookone\n'
    else
        printf '  Claude Code was not detected. Install it, then re-run this installer to\n'
        printf '  auto-register the "rookone" MCP server (or run it yourself):\n'
        printf '    claude mcp add --scope user rookone -- rookone-channel\n'
    fi
    printf '\n'
    printf 'Docs: https://github.com/eigentic/rookone-cli\n'
    printf '\n'
    printf 'Your identity now lives in ~/.rookone — keep that directory to keep your agent.\n'
    printf '\n'
}

# ---------------------------------------------------------------------------
# Purge / uninstall
# ---------------------------------------------------------------------------

usage() {
    cat >&2 <<'USAGE'
RookOne installer.

Usage:
    sh install.sh                  Install or upgrade rookone + rookone-channel.
    sh install.sh --purge          Uninstall BOTH tools from uv AND pipx and
                                   clear our uv cache. Leaves ~/.rookone intact.
    sh install.sh --purge-identity Same as --purge, then ALSO delete ~/.rookone
                                   (agent identity + local message archive).
                                   Irreversible.
    sh install.sh --help           Show this help.
USAGE
}

# Remove rookone + rookone-channel from BOTH package managers (uv is canonical;
# pipx covers legacy/migration leftovers), plus our uv cache. rookone and
# rookone-channel are two SEPARATE uv tools, so both must be removed explicitly.
# ~/.rookone is the user's identity + message archive — only deleted when
# PURGE_IDENTITY=1, NEVER silently.
purge() {
    log_info "purge: removing rookone + rookone-channel (uv + pipx)"

    if command -v uv >/dev/null 2>&1; then
        for _pg_pkg in rookone rookone-channel; do
            if uv tool uninstall "$_pg_pkg" >/dev/null 2>&1; then
                log_info "removed uv tool: $_pg_pkg"
            fi
        done
        # Drop cached wheels for our packages so a reinstall can't resurrect a
        # bad build from cache.
        uv cache clean rookone rookone-channel rookone-sdk rookone-shared rookone-core >/dev/null 2>&1 || true
        # Defensive sweep: remove any tool dirs uninstall left behind.
        _pg_tooldir=$(uv tool dir 2>/dev/null | tr -d '\n')
        if [ -n "$_pg_tooldir" ] && [ -d "$_pg_tooldir" ]; then
            for _pg_d in "$_pg_tooldir/rookone" "$_pg_tooldir/rookone-channel"; do
                if [ -e "$_pg_d" ]; then
                    rm -rf "$_pg_d" && log_info "swept leftover uv tool dir: $_pg_d"
                fi
            done
        fi
    fi

    if command -v pipx >/dev/null 2>&1; then
        for _pg_pkg in rookone rookone-channel; do
            if pipx uninstall "$_pg_pkg" >/dev/null 2>&1; then
                log_info "removed pipx package: $_pg_pkg"
            fi
        done
    fi

    log_success "rookone + rookone-channel removed."

    if [ "${PURGE_IDENTITY:-0}" = "1" ]; then
        if [ -d "$HOME/.rookone" ]; then
            rm -rf "$HOME/.rookone"
            log_warning "Deleted ~/.rookone — agent identity + local message archive are gone."
        else
            log_info "The ~/.rookone directory was not present."
        fi
    else
        log_info "Left ~/.rookone intact (agent identity + local message archive)."
        log_info "  To also erase it (IRREVERSIBLE): re-run with --purge-identity"
    fi
}

# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

main() {
    PURGE=0
    PURGE_IDENTITY=0
    while [ $# -gt 0 ]; do
        case "$1" in
            --purge)          PURGE=1 ;;
            --purge-identity) PURGE=1; PURGE_IDENTITY=1 ;;
            -h|--help)        usage; exit 0 ;;
            *) log_error "unknown option: $1"; usage; exit 2 ;;
        esac
        shift
    done

    if [ "$PURGE" = "1" ]; then
        purge
        exit 0
    fi

    log_info "RookOne installer starting"
    _rk_require_https "$ROOKONE_INDEX_URL" || exit 1
    ensure_uv
    install_rookone
    setup_claude_mcp
    print_next_steps
}

# Skip main when sourced as a library (the test harness sets this) so the
# detection/messaging functions can be exercised without bootstrapping uv or
# installing anything.
if [ -z "${ROOKONE_INSTALL_LIB:-}" ]; then
    main "$@"
fi
