#!/bin/bash
# Railcall network installer.  Usage:
#   curl -fsSL https://raw.githubusercontent.com/patl4588/railcall-cli/main/install.sh | bash

# ── bash guard (must stay POSIX sh and must stay ABOVE `set -o pipefail`) ──────────────────────
# This script is bash. People copy-paste `curl … | sh` anyway, and on Debian/Ubuntu/WSL `sh` is
# dash, which dies on the first ${BASH_SOURCE[0]} with "Bad substitution" (seen in the field
# 2026-08-23). macOS never showed it because /bin/sh there IS bash, so the docs drifted to `| sh`
# unnoticed. Re-exec under bash instead of failing:
#   · run from a file  → exec bash on that file;
#   · piped on stdin   → we CANNOT read the rest of stdin (dash has already buffered ~8 KB past
#     this point — measured — so `cat` would hand bash a script starting mid-line). Re-fetch the
#     canonical copy over HTTPS and exec bash on that. Every core file is still sha-pinned inside
#     the script, so the re-fetch changes nothing about what gets trusted.
if [ -z "${BASH_VERSION:-}" ]; then
    if [ -f "$0" ] && [ "$0" != "sh" ] && [ "$0" != "dash" ]; then
        exec bash "$0" "$@"
    fi
    echo "RailCall: this installer needs bash, not sh — re-running it under bash." >&2
    _rc_src=""
    for _u in https://railcall.ai/install.sh \
              https://raw.githubusercontent.com/patl4588/railcall-cli/main/install.sh; do
        _rc_src="$(curl -fsSL "$_u" 2>/dev/null)" && [ -n "$_rc_src" ] && break
        _rc_src=""
    done
    if [ -z "$_rc_src" ] || ! command -v bash >/dev/null 2>&1; then
        echo "RailCall: could not re-run under bash automatically. Please run:" >&2
        echo "    curl -fsSL https://railcall.ai/install.sh | bash" >&2
        exit 1
    fi
    exec bash -c "$_rc_src" bash "$@"
fi
set -euo pipefail

# A restrictive umask (corporate hardening often sets 077 or 177) makes mkdir
# create directories WITHOUT the owner-execute bit — rw------- — which cannot
# be traversed, so the very next mkdir inside it fails with a baffling
# "Permission denied" on a directory the user owns. Seen in the field (umask
# 0177). Pin a sane umask for this process only; secrets still get their
# explicit chmod 600, so nothing becomes less private.
umask 022

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

echo -e "${CYAN}================================================================${NC}"
echo -e "${CYAN}                 R A I L C A L L   I N S T A L L E R            ${NC}"
echo -e "${CYAN}================================================================${NC}"

# SOURCES. Every byte from every source is verified against the sha256 pinned below, so a
# mirror can never inject anything — that is the entire point of pinning, and it is why
# adding one is safe. We ship TWO because raw.githubusercontent.com is blocked or
# transparently proxied on some networks (corporate MITM, national filtering, hotel/ISP
# captive portals): those return a *different body* that fails the pin, which is the gate
# working correctly, not a bad pin. railcall.ai is our own origin and serves byte-identical
# copies, so it is a fallback we control rather than a third-party CDN.
RAW_BASE="https://raw.githubusercontent.com/patl4588/railcall-cli/main"
MIRROR_BASE="https://railcall.ai/cli"
# Install locations. Both are overridable so RailCall can be installed on a
# machine where $HOME is not usable as an install target — a managed/network
# home that re-applies ACLs, a locked-down corporate profile, or a home dir on
# a read-only or full volume. Set either before running:
#
#   RAILCALL_HOME=/opt/railcall ./install.sh
#   RAILCALL_HOME="$HOME/rc" RAILCALL_CONF="$HOME/rc/conf" ./install.sh
#
# Everything below refers to $RC_HOME / $RC_CONF — never to a hardcoded
# $HOME/.railcall — so an override actually relocates the whole install
# including the generated launcher.
RC_HOME="${RAILCALL_HOME:-$HOME/.railcall}"
RC_BIN="$RC_HOME/bin"
RC_CONF="${RAILCALL_CONF:-$HOME/.config/railcall}"
FILES="railcall_cli.py railcall_companion_daemon.py vault_io.py receipt_signer.py railcall_vault_drivers.py"
GOVERNANCE_FILES="governance/__init__.py governance/policy_engine.py governance/policy_schema.py governance/receipt_v2.py governance/defaults/__init__.py governance/defaults/governance.default.yml"
STATION_SHA="e4282e6918e9037b83b3b2509fad3c5dcb5e88b5faa6a55835850473c641fadf"

# Full disclosure BEFORE the first write — everything this installer touches, up front:
echo -e "${BLUE}This installer writes to:${NC}"
echo -e "${BLUE}  · $RC_HOME — the CLI, the 'railcall' launcher, and the Studio bundle (~5MB download)${NC}"
echo -e "${BLUE}  · $RC_CONF — your pre-login local trial token (token.json, owner-only chmod 600)${NC}"
echo -e "${BLUE}  · $HOME/Desktop — a double-click 'RailCall Studio.command' launcher (only if a Desktop folder exists)${NC}"
echo -e "${BLUE}  · your shell rc (.zshrc / .bashrc / .bash_profile) — one PATH line, only if one of those files exists${NC}"
echo -e "${BLUE}  · Python user packages — the 'cryptography' package via pip --user, announced below, only if missing${NC}"

# Pre-flight: the install dirs must be writable by the CURRENT user, and a bare
# "mkdir: .../bin: Permission denied" tells the operator nothing about why.
#
# There are FOUR independent reasons a directory in $HOME won't accept a write,
# and they need different fixes. Reporting only ownership is what sent one
# operator in a loop: they ran the chown we suggested, it succeeded, and the
# install still failed — because the directory was owned by them all along and
# the missing piece was the write MODE. chown does not restore mode, and on
# macOS it does not clear the immutable flag or an ACL either.
#
# So: detect which one it actually is and print only the fix that applies.
# NOTE: this whole script runs under `set -euo pipefail`, so every test here
# must be inside an if-condition (or carry `|| true`). A bare `[ ... ] && ...`
# chain that evaluates false at top level KILLS the script with no output —
# which is exactly how the first version of this preflight failed in the
# field: it died silently on the very directory state it was written to
# diagnose.
me=$(id -un)
for d in "$RC_HOME" "$RC_CONF"; do
    if [ ! -e "$d" ]; then continue; fi
    if [ -w "$d" ] && [ -x "$d" ]; then continue; fi

    owner=$(ls -ld "$d" | awk '{print $3}' || true)
    mode=$(ls -ld "$d" | awk '{print $1}' || true)
    flags=$(ls -ldO "$d" 2>/dev/null | awk '{print $5}' || true)
    has_acl=$(ls -lde "$d" 2>/dev/null | grep -c '^ *[0-9]*: ' || true)

    echo ""
    echo -e "${RED}✗ Cannot write into $d${NC}"
    echo ""
    echo "    owner : $owner    (you are: $me)"
    echo "    mode  : $mode"
    if [ -n "$flags" ] && [ "$flags" != "-" ]; then echo "    flags : $flags"; fi
    if [ "${has_acl:-0}" != "0" ]; then echo "    ACL   : present"; fi
    echo ""

    if [ "$owner" != "$me" ]; then
        echo "  The directory belongs to another user. This is usually left behind"
        echo "  by an earlier 'sudo ./install.sh' — RailCall is a per-user install"
        echo "  and must NOT be run with sudo."
    else
        echo "  You already own this directory, so ownership is not the problem —"
        echo "  the write/traverse permission itself is missing (mode/flag/ACL)."
    fi

    echo ""
    echo "  Run these, then re-run the installer WITHOUT sudo:"
    echo ""
    if [ -n "$flags" ] && [ "$flags" != "-" ]; then
        echo "      sudo chflags -R nouchg \"$d\""
    fi
    if [ "${has_acl:-0}" != "0" ]; then
        echo "      sudo chmod -RN \"$d\""
    fi
    if [ "$owner" != "$me" ]; then
        echo "      sudo chown -R \"\$(id -un)\" \"$d\""
    fi
    echo "      chmod -R u+rwX \"$d\""
    echo ""
    exit 1
done

# Refuse to run as root in the first place, so we never create the state above.
if [ "$(id -u)" = "0" ] && [ -n "${SUDO_USER:-}" ]; then
    echo ""
    echo -e "${RED}✗ Do not run this installer with sudo.${NC}"
    echo "  RailCall installs per-user into \$HOME. Running as root creates"
    echo "  root-owned files in $SUDO_USER's home that later runs cannot write."
    echo ""
    echo "  Re-run as yourself:   ./install.sh"
    echo ""
    exit 1
fi

mkdir -p "$RC_HOME" "$RC_BIN" "$RC_CONF"
mkdir -p "$RC_HOME/transaction_runs"
mkdir -p "$RC_HOME/library/promotions"

# Seed the promotions registry. Non-fatal and non-destructive:
#   - an existing registry is KEPT (a reinstall must not wipe promoted legos);
#   - an unwritable target file (e.g. a root-owned leftover from an old sudo
#     install — seen in the field) warns instead of killing the whole install
#     under `set -e`. The registry is an optional seed; the CLI tolerates its
#     absence.
LEGOS="$RC_HOME/library/promotions/governed_legos_registry.json"
if [ ! -f "$LEGOS" ]; then
    if ! cp -f library/promotions/governed_legos_registry.json "$LEGOS" 2>/dev/null; then
        if ! echo '{"governed_legos": [], "version": "1.0", "note": "Add promoted workflow legos here"}' > "$LEGOS" 2>/dev/null; then
            echo -e "${RED}  ! could not write $LEGOS — continuing without it.${NC}"
            echo -e "${RED}    If a file already exists there from an old sudo install, remove it:${NC}"
            echo -e "${RED}      sudo rm -f \"$LEGOS\"${NC}"
        fi
    fi
fi

# Pick a downloader (-f makes curl FAIL on a 404 instead of saving the error page).
if command -v curl >/dev/null 2>&1; then
    fetch() { curl -fsSL "$1" -o "$2"; }
elif command -v wget >/dev/null 2>&1; then
    fetch() { wget -q -O "$2" "$1"; }
else
    echo -e "${RED}Need curl or wget to install.${NC}"; exit 1
fi

# Resolve ONE Python 3 interpreter, used consistently below (py_compile validation, cryptography,
# the launcher). Windows Git-Bash often ships only 'python' — accept it if it is Python 3.
PY=""
if command -v python3 >/dev/null 2>&1; then
    PY=python3
elif command -v python >/dev/null 2>&1 && python -c "import sys; sys.exit(0 if sys.version_info[0]==3 else 1)" >/dev/null 2>&1; then
    PY=python
fi
if [ -z "$PY" ]; then
    echo -e "${RED}Python 3 is required and was not found on PATH (looked for 'python3', then 'python').${NC}"; exit 1
fi

# If this installer is being run from a repo checkout (git clone / unzipped ZIP), the source files sit
# right next to it — use those first so a fully offline / region-blocked install just works.
# dirname of a bare 'install.sh' is '.' → the invoker's cwd; that is safe even for piped installs
# (curl|bash) because fetch_valid only trusts a local file that exists non-empty AND py_compiles.
SELF="${BASH_SOURCE[0]:-$0}"
LOCAL_DIR="$(cd "$(dirname "$SELF")" 2>/dev/null && pwd)" || LOCAL_DIR=""

# ---- Supply-chain integrity pins ------------------------------------------------------------------
# Every core file is verified against a sha256 that is PINNED into this installer. This stops a
# compromised 'main' (or a MITM proxy that swaps the body) from injecting code that merely happens to
# compile — a file whose bytes do not match its pin is REFUSED and never installed, even if py_compile
# passes. The hash gate is ADDITIONAL to the existing non-empty + py_compile checks, not a replacement.
#
# Regenerate these pins after an INTENTIONAL change to the core files, from a repo checkout, with:
#   for f in railcall_cli.py railcall_companion_daemon.py vault_io.py receipt_signer.py; do \
#     printf '        %-30s echo %s ;;\n' "$f)" "$(shasum -a 256 "$f" | awk '{print $1}')"; done
#   # (on Linux use `sha256sum "$f"` instead of `shasum -a 256 "$f"`)
# then paste the printed lines over the case arms in pin_for() below.
pin_for() {
    case "$1" in
        railcall_cli.py)                          echo af3f3adb0410b13aa3cf4aa8c1755df500dd209bccfc7c5fc9f96e140bc057e3 ;;
        railcall_companion_daemon.py)             echo f6a43720157612adbc73723115166fbe3acf8e43f0113ea717cca27b9990a1b5 ;;
        vault_io.py)                              echo 17b0e644a93c773d3f7b5e5e8b046ea39472364b532b545846f3c617433792f8 ;;
        receipt_signer.py)                        echo 36b84579880db9bf78c9bc21cd40c6976094ae8ea978c939f2feef4f97041b9e ;;
        railcall_vault_drivers.py)                echo 8e14624eaefde16684ca1ca75782ad835e91f4736e568e78d40adc61b0a22867 ;;
        governance/__init__.py)                   echo a039118f68adec79c887c26f3a7218b0096da47bb18c7efb13e52f06af94cedd ;;
        governance/policy_engine.py)              echo 6518840af666c2bcffe53b8bc73c19d7ad3c933fdede5bdc6c7dfe9dfdc831fb ;;
        governance/policy_schema.py)              echo 943b777cef4c8a776490a0e5950885180f8d2e815bdeee4c7866c4022ee9410a ;;
        governance/receipt_v2.py)                 echo fad0581fe6e6780608c78fec9a124eb1a833159067107f4ff313c6ba459971c6 ;;
        governance/defaults/__init__.py)          echo 5d16591a5456de8b492aa701a1f7b989040995513fc813600d0d445b24131e34 ;;
        governance/defaults/governance.default.yml) echo ff56072e81ed4908ea91f567741238b387e536cd1f5974513ee18df0d5c575b9 ;;
        *) echo "" ;;
    esac
}

# Portable sha256 of a file → stdout. Linux ships sha256sum; macOS/BSD ship shasum. Empty if neither.
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
}

# Verify a file on disk against its pin. Non-zero (with a LOUD security refusal) on any mismatch,
# an unpinned filename, or when no sha256 tool exists — we would rather refuse than install unverified
# code. A pass here means the bytes are exactly what we published.
pin_ok() {
    f="$1"; path="$2"; want="$(pin_for "$f")"
    if [ -z "$want" ]; then
        echo -e "${RED}  ✗ SECURITY: $f has no integrity pin in this installer — refusing to install unpinned code.${NC}"; return 1
    fi
    got="$(sha256_of "$path")"
    if [ -z "$got" ]; then
        echo -e "${RED}  ✗ SECURITY: cannot hash $f — no sha256sum/shasum tool found. Refusing to install unverified code.${NC}"; return 1
    fi
    if [ "$got" != "$want" ]; then
        # QUIET per source. Trying the next source is the normal recovery path, so a
        # SUCCESSFUL install would otherwise print a red SECURITY block followed by a
        # green tick — which reads as a breach and led a user to conclude the pins were
        # stale. The loud message belongs in the caller, once, only if EVERY source fails.
        LAST_PIN_FAIL="$f expected $want got $got"
        return 1
    fi
    return 0
}

# Get + validate one file: try the local checkout, then raw GitHub only (CDN removed).
# A file only counts if it is non-empty AND compiles as Python AND matches its pinned sha256 —
# so a proxy's fake "404: Not Found" body is rejected (fails compile) and any tampered-but-compiling
# body is refused by the pin. No fallback sources.
fetch_valid() {
    f="$1"; dest="$RC_HOME/$f"; LAST_PIN_FAIL=""
    # Atomic-ish updates: fetch to a temp path, only mv into $dest AFTER
    # pin_ok passes. Without this a `railcall update` that catches the
    # release mid-propagation (install.sh has new pin, GitHub raw / mirror
    # still serving old bytes) would rm the good local copy and leave the
    # machine with no CLI, breaking every subsequent invocation. The
    # temp copy is always cleaned up.
    tmp="${dest}.new.$$"
    if [ -n "$LOCAL_DIR" ] && [ -s "$LOCAL_DIR/$f" ] && "$PY" -m py_compile "$LOCAL_DIR/$f" 2>/dev/null; then
        if pin_ok "$f" "$LOCAL_DIR/$f"; then
            mkdir -p "$(dirname "$dest")"
            cp "$LOCAL_DIR/$f" "$dest"; echo -e "${GREEN}  ✓ $f${BLUE} (local checkout)${NC}"; return 0
        fi
    fi
    for base in "$RAW_BASE" "$MIRROR_BASE"; do
        mkdir -p "$(dirname "$tmp")"
        if fetch "$base/$f" "$tmp" 2>/dev/null && [ -s "$tmp" ] && "$PY" -m py_compile "$tmp" 2>/dev/null && pin_ok "$f" "$tmp"; then
            mv -f "$tmp" "$dest"
            if [ "$base" = "$MIRROR_BASE" ]; then
                echo -e "${GREEN}  ✓ $f${BLUE} (via railcall.ai — your network altered the GitHub copy)${NC}"
            else
                echo -e "${GREEN}  ✓ $f${NC}"
            fi
            return 0
        fi
        rm -f "$tmp"
    done
    # Reached here only when EVERY source failed. If a good copy already
    # exists at $dest (upgrade path from a previous successful install),
    # keep it — a partial-release race must never take down a working
    # install. The caller still returns non-zero so the operator sees a
    # clear "could not update" message.
    if [ -s "$dest" ]; then
        echo -e "${BLUE}  · $f — keeping existing verified copy; update deferred${NC}"
    fi
    return 1
}

echo -e "${BLUE}Downloading CLI (pinned + sha256-verified; falls back to railcall.ai) ...${NC}"
for f in $FILES; do
    if ! fetch_valid "$f"; then
        echo -e "${RED}✗ Could not fetch a valid $f from GitHub (raw).${NC}"
        echo -e "${RED}  (If a SECURITY integrity-pin refusal printed above, STOP — do not work around it; the${NC}"
        echo -e "${RED}   published bytes did not match this installer's pin. Otherwise this is almost always a${NC}"
        echo -e "${RED}   regional network block on raw.githubusercontent.com${NC}"
        echo -e "${RED}  (some ISPs return a fake page). Two ways around it:${NC}"
        echo -e "${BLUE}  1) Fix DNS:${NC}"
        echo -e "${BLUE}     WSL/Linux:            echo \"nameserver 8.8.8.8\" | sudo tee /etc/resolv.conf${NC}"
        echo -e "${BLUE}     Git Bash (Windows):   DNS lives in Windows, not the shell. Admin PowerShell:${NC}"
        echo -e "${BLUE}       Set-DnsClientServerAddress -InterfaceAlias \"Wi-Fi\" -ServerAddresses 8.8.8.8${NC}"
        echo -e "${BLUE}       (or Settings > Network > your adapter > DNS servers > 8.8.8.8), then re-run.${NC}"
        echo -e "${BLUE}  2) Install from a clone (works everywhere, incl. Git Bash):${NC}"
        echo -e "${BLUE}                           git clone https://github.com/patl4588/railcall-cli${NC}"
        echo -e "${BLUE}                           cd railcall-cli && bash install.sh${NC}"
        exit 1
    fi
done
chmod +x "$RC_HOME/railcall_cli.py"

# Phase 1: governance package — install alongside the CLI files so the policy engine is available.
echo -e "${BLUE}Installing governance policy engine (Phase 1) ...${NC}"
mkdir -p "$RC_HOME/governance/defaults"
for f in $GOVERNANCE_FILES; do
    dest="$RC_HOME/$f"; LAST_PIN_FAIL=""
    if [ -n "$LOCAL_DIR" ] && [ -s "$LOCAL_DIR/$f" ] && pin_ok "$f" "$LOCAL_DIR/$f"; then
        cp "$LOCAL_DIR/$f" "$dest"; echo -e "${GREEN}  ✓ $f${BLUE} (local checkout)${NC}"; continue
    fi
    _got=""
    for base in "$RAW_BASE" "$MIRROR_BASE"; do
        if fetch "$base/$f" "$dest" 2>/dev/null && [ -s "$dest" ] && pin_ok "$f" "$dest"; then
            if [ "$base" = "$MIRROR_BASE" ]; then
                echo -e "${GREEN}  ✓ $f${BLUE} (via railcall.ai)${NC}"
            else
                echo -e "${GREEN}  ✓ $f${NC}"
            fi
            _got=1; break
        fi
        rm -f "$dest"
    done
    [ -n "$_got" ] && continue
    [ -n "$LAST_PIN_FAIL" ] && echo -e "${RED}  ✗ $f — every source returned bytes not matching our published hash (${LAST_PIN_FAIL}). Your network is probably modifying downloads; the check is working. Do NOT edit the pins.${NC}"
    echo -e "${RED}✗ Could not fetch a valid $f — governance policy engine will not be available.${NC}"
    echo -e "${RED}  Receipts will still be written but policy gating is disabled on this install.${NC}"
done

# Ed25519 receipt signing needs `cryptography`. Best-effort + NON-FATAL: without it the daemon still
# writes airlock-verified, SHA-256 receipts — just honestly UNSIGNED. With it, every receipt is signed.
# We VERIFY the import after each attempt (pip's exit code alone is not proof it's importable), and on
# an "externally-managed-environment" Python (PEP 668 — Homebrew python, Debian/Ubuntu system python)
# a plain `pip install --user` is refused, so we retry with --break-system-packages (the supported
# escape hatch for a user-site install). If signing still can't be enabled we say so LOUDLY rather than
# leaving the user to discover unsigned receipts later.
crypto_ok() { "$PY" -c "import cryptography" >/dev/null 2>&1; }
if crypto_ok; then
    echo -e "${GREEN}  ✓ receipt signing available (Ed25519)${NC}"
else
    echo -e "${BLUE}  · installing the Python 'cryptography' package so receipts can be Ed25519-signed ...${NC}"
    PIP_USER="$PY -m pip install --user --quiet --disable-pip-version-check"
    $PIP_USER cryptography >/dev/null 2>&1 || true
    if ! crypto_ok; then
        # PEP 668 externally-managed-environment (Homebrew / Debian system python): retry with the escape hatch.
        $PIP_USER --break-system-packages cryptography >/dev/null 2>&1 || true
    fi
    if crypto_ok; then
        echo -e "${GREEN}  ✓ receipt signing enabled (installed cryptography)${NC}"
    else
        echo -e "${RED}  ! receipt signing is NOT enabled — receipts will be written UNSIGNED (airlock-verified, SHA-256 only).${NC}"
        echo -e "${RED}    'cryptography' could not be installed automatically (usually PEP 668 on a Homebrew/system Python).${NC}"
        echo -e "${BLUE}    Turn on signing with ONE of these, then re-run this installer:${NC}"
        echo -e "${CYAN}      $PY -m pip install --user --break-system-packages cryptography${NC}"
        echo -e "${CYAN}      pipx install cryptography${NC}    ${BLUE}# if you use pipx${NC}"
        echo -e "${BLUE}    (verify with:  $PY -c \"import cryptography\"  — no output means it's ready)${NC}"
    fi
fi

# ---- Studio (the visual builder) — fetch + unpack the station bundle (one-time, ~22MB) ----
STATION_URL="https://github.com/patl4588/railcall-core/releases/download/station-v1.5.40/railcall_station.tar.gz"
# Version reported by the telemetry ping (below) is derived from the pinned
# STATION_URL so it always matches the actual cut being installed. publish-
# release.sh re-pins STATION_URL every release, so this can never go stale the
# way the old hardcoded string did.
STATION_VERSION="$(printf '%s' "$STATION_URL" | sed -n 's#.*/download/station-v\([^/]*\)/.*#\1#p')"
[ -n "$STATION_VERSION" ] || STATION_VERSION="unknown"
# Mirror on our own origin. The tarball had ONE source, so a network that rewrites or
# blocks github.com failed the install outright even after the CLI files recovered.
# STATION_SHA is enforced identically on whichever source answers, so the mirror cannot
# substitute a different bundle.
#
# ?v=$STATION_SHA is a cache-buster — some middleboxes (edge caches, ISP
# proxies) hold onto the URL for hours even when we set Cache-Control:
# max-age=0 on the response. Baking the pinned SHA into the query string
# makes every release a distinct URL so a stale copy from an older
# release can never be served under this version's key.
STATION_URL_MIRROR="https://railcall.ai/railcall_station.tar.gz?v=$STATION_SHA"
STATION_DIR="$RC_HOME/station"
echo -e "${BLUE}Downloading the RailCall Studio (one-time, ~22MB) ...${NC}"
station_get() {
    # Air-gap path first: a local station.tar.gz next to install.sh wins over
    # any network fetch. Same STATION_SHA gate — even the local copy is refused
    # if bytes don't match the pin, so an air-gap bundle can't smuggle in a
    # different station than the one this installer was minted for.
    if [ -n "$LOCAL_DIR" ] && [ -s "$LOCAL_DIR/railcall_station.tar.gz" ]; then
        a=$(sha256_of "$LOCAL_DIR/railcall_station.tar.gz")
        if [ "$a" = "$STATION_SHA" ]; then
            cp "$LOCAL_DIR/railcall_station.tar.gz" "$RC_HOME/station.tar.gz"
            echo -e "${BLUE}  · loaded from local bundle (air-gap install)${NC}"
            return 0
        fi
        STATION_GOT="$a"
        # This is the AIR-GAP path only: a railcall_station.tar.gz sitting next
        # to install.sh. The overwhelmingly common cause is a bundle left over
        # from an EARLIER release — completely benign, because we ignore it and
        # fetch the pinned copy instead.
        #
        # The old message was one red ✗ line saying "sha mismatch", which named
        # no file, showed no shas, and did not say recovery was automatic. Users
        # read it as "my install is broken/compromised" and reported it as a
        # failure — on installs that then completed perfectly. A warning that
        # cannot be told apart from a real failure trains people to ignore both.
        echo -e "${YELLOW}  ! ignoring the local station bundle — it is not the one this${NC}"
        echo -e "${YELLOW}    installer is pinned to, so it will NOT be used:${NC}"
        echo -e "${YELLOW}      file:     $LOCAL_DIR/railcall_station.tar.gz${NC}"
        echo -e "${YELLOW}      its sha:  $a${NC}"
        echo -e "${YELLOW}      pinned:   $STATION_SHA${NC}"
        echo -e "${BLUE}    Almost always this is a bundle from an older release. Downloading${NC}"
        echo -e "${BLUE}    the pinned one now — nothing is wrong and no action is needed.${NC}"
        echo -e "${BLUE}    (Doing a deliberate air-gap install? Replace that file with the${NC}"
        echo -e "${BLUE}     bundle whose sha matches 'pinned' above.)${NC}"
    fi
    for u in "$STATION_URL" "$STATION_URL_MIRROR"; do
        # 2>/dev/null suppresses curl's own "404" chatter on the first
        # source — the mirror is the normal recovery path (GitHub release
        # tags trail behind the pinned STATION_URL for a few days after
        # each cut), so a stderr leak from the first attempt reads to the
        # user as a broken install even when the second attempt succeeds.
        # If BOTH sources fail we emit a clear message below.
        fetch "$u" "$RC_HOME/station.tar.gz" 2>/dev/null || continue
        a=$(sha256_of "$RC_HOME/station.tar.gz")
        if [ "$a" = "$STATION_SHA" ]; then
            [ "$u" = "$STATION_URL_MIRROR" ] && echo -e "${BLUE}  · fetched via railcall.ai (GitHub release not yet uploaded for this pin)${NC}"
            return 0
        fi
        STATION_GOT="$a"
        rm -f "$RC_HOME/station.tar.gz"
    done
    return 1
}
if station_get; then
    mkdir -p "$STATION_DIR"
    if tar -xzf "$RC_HOME/station.tar.gz" -C "$STATION_DIR" 2>/dev/null && [ -f "$STATION_DIR/workbench/studio_server.py" ]; then
        rm -f "$RC_HOME/station.tar.gz"
        echo -e "${GREEN}  ✓ Studio installed — run 'railcall studio' to open it in your browser.${NC}"
    else
        rm -f "$RC_HOME/station.tar.gz"
        echo -e "${RED}  ✗ Studio archive downloaded but failed to unpack (CLI still works; re-run the installer for the Studio).${NC}"
    fi
else
    if [ -n "$STATION_GOT" ]; then
        echo -e "${RED}  ✗ SECURITY: the Studio bundle failed its integrity check from every source.${NC}"
        echo -e "${RED}      expected $STATION_SHA${NC}"
        echo -e "${RED}      got      $STATION_GOT${NC}"
        echo -e "${RED}      Your network is probably modifying downloads — the check is working.${NC}"
    else
        echo -e "${RED}  ✗ Could not download the Studio bundle (CLI still works; re-run the installer to retry the Studio).${NC}"
    fi
fi

# Pre-login LOCAL trial token. REAL enforcement state: the CLI reads token["runs_remaining"],
# decrements it per build, and hard-blocks at 0. Re-running never resets an existing token.
# The rc_local_ prefix is a LOCAL sentinel the engine allowlists — it must NEVER touch the gateway.
TOKEN_FILE="$RC_CONF/token.json"
chmod 700 "$RC_CONF" 2>/dev/null || true
if [ ! -f "$TOKEN_FILE" ]; then
    echo '{"api_key": "rc_local_trial_500", "tier": "free", "runs_remaining": 500}' > "$TOKEN_FILE"
    echo -e "${GREEN}Provisioned a pre-login LOCAL trial of 500 flows — enforced by the CLI on this machine only, never a hosted balance.${NC}"
    echo -e "${GREEN}It is replaced by your account balance the moment you run 'railcall login <key>' (free accounts include 500 flows, refilled monthly).${NC}"
else
    echo -e "${GREEN}Existing token kept (not reset).${NC}"
fi
chmod 600 "$TOKEN_FILE" 2>/dev/null || true   # BYOK token file must be owner-only

# Thin wrapper: forward EVERY command + arg straight to the real CLI. No fake telemetry.
# Bakes in the interpreter resolved above ($PY) so 'python'-only setups (Git-Bash) keep working.
cat > "$RC_BIN/railcall" << WRAP
#!/bin/bash
exec $PY "$RC_HOME/railcall_cli.py" "\$@"
WRAP
chmod +x "$RC_BIN/railcall"

# Double-click launcher (macOS): a clickable "RailCall Studio" on the Desktop that opens the Studio.
if [ -d "$HOME/Desktop" ]; then
    LAUNCHER="$HOME/Desktop/RailCall Studio.command"
    printf '#!/bin/bash\nexec "%s" studio\n' "$RC_BIN/railcall" > "$LAUNCHER"
    chmod +x "$LAUNCHER"
    echo -e "${GREEN}  ✓ Double-click 'RailCall Studio' on your Desktop to open the Studio anytime.${NC}"
fi

# Add the bin dir to PATH persistently.
# Special handling for Git Bash / MINGW64 / MSYS on Windows (the environment the reporter
# used): interactive shells source ~/.bashrc, and the default Git Bash setup often relies
# on it. We force .bashrc for MINGW and also touch .bash_profile if it exists.
# This ensures the 'railcall' wrapper stays in PATH after closing the terminal.
SHELL_CONFIG=""
if [[ "${OSTYPE:-}" == msys* || "${OSTYPE:-}" == cygwin* || -n "${MSYSTEM:-}" ]]; then
    # Windows Git Bash / MINGW64 / MSYS2
    SHELL_CONFIG="$HOME/.bashrc"
    # Also ensure .bash_profile exists and will source .bashrc (common Git Bash pattern)
    if [ ! -f "$HOME/.bash_profile" ]; then
        echo '# Git Bash default' > "$HOME/.bash_profile"
    fi
    if ! grep -q 'source ~/.bashrc' "$HOME/.bash_profile" 2>/dev/null; then
        echo 'if [ -f ~/.bashrc ]; then . ~/.bashrc; fi' >> "$HOME/.bash_profile"
    fi
elif [ -f "$HOME/.zshrc" ]; then
    SHELL_CONFIG="$HOME/.zshrc"
elif [ -f "$HOME/.bashrc" ]; then
    SHELL_CONFIG="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
    SHELL_CONFIG="$HOME/.bash_profile"
fi

if [ -n "$SHELL_CONFIG" ]; then
    mkdir -p "$(dirname "$SHELL_CONFIG")" 2>/dev/null || true
    if [ ! -f "$SHELL_CONFIG" ]; then
        touch "$SHELL_CONFIG"
    fi
    if ! grep -q "$RC_BIN" "$SHELL_CONFIG" 2>/dev/null; then
        echo "" >> "$SHELL_CONFIG"
        echo "# Added by Railcall installer (supports Git Bash/MINGW on Windows)" >> "$SHELL_CONFIG"
        echo "export PATH=\"\$PATH:$RC_BIN\"" >> "$SHELL_CONFIG"
        echo -e "${GREEN}Added $RC_BIN to PATH in $SHELL_CONFIG${NC}"
    fi
fi

# ── anonymous install ping ───────────────────────────────────────────────
# One POST at the tail of a successful install: a stable per-machine hash
# (sha256 of hostname + install-time salt cached in ~/.railcall/machine_id),
# the version, OS, and arch. NO hostname, no username, no path, no IP —
# we hash our end before sending. This is a counter, not tracking.
# Skip entirely with RAILCALL_NO_TELEMETRY=1.
if [ -z "${RAILCALL_NO_TELEMETRY:-}" ]; then
    MACHINE_ID_FILE="$RC_HOME/machine_id"
    if [ ! -f "$MACHINE_ID_FILE" ]; then
        # Prefer python (already required by the CLI) for a random hex id.
        # Falls back to /dev/urandom if python is somehow gone by this point.
        if command -v "$PY" >/dev/null 2>&1; then
            MID=$("$PY" -c "import secrets;print(secrets.token_hex(32))" 2>/dev/null || echo "")
        fi
        if [ -z "${MID:-}" ]; then
            MID=$(head -c 32 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' 2>/dev/null || echo "")
        fi
        if [ -n "$MID" ]; then
            echo "$MID" > "$MACHINE_ID_FILE"
            chmod 600 "$MACHINE_ID_FILE" 2>/dev/null || true
        fi
    fi
    MID=$(cat "$MACHINE_ID_FILE" 2>/dev/null || true)
    OS=$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')
    ARCH=$(uname -m 2>/dev/null || echo unknown)
    # First-party install attribution (2026-08-31). `curl | sh` sends no HTTP
    # referrer, so a web log can only attribute the browser slice and every
    # source ends up under-credited. Instead the operator's own install URL
    # carries the tag — RAILCALL_SRC=x, or railcall.ai/install.sh?src=x which
    # the site forwards as that env var — and it rides the COMPLETED-install
    # ping below. Sanitised to [A-Za-z0-9._-] and capped at 64 chars; empty
    # when absent, which the backend reports honestly as "(untagged)".
    SRC=$(printf '%s' "${RAILCALL_SRC:-}" | tr -cd 'A-Za-z0-9._-' | cut -c1-64)
    if [ -n "${MID:-}" ]; then
        # Fire-and-forget: 3s timeout, silent on failure. install.sh completes
        # regardless — a marketplace outage does NOT block a user install.
        curl -fsS --max-time 3 -o /dev/null \
            -X POST -H "Content-Type: application/json" \
            -d "{\"machine_id\":\"$MID\",\"version\":\"$STATION_VERSION\",\"station_sha\":\"$STATION_SHA\",\"os\":\"$OS\",\"arch\":\"$ARCH\",\"src\":\"$SRC\"}" \
            "https://railcall-marketplace-lggm.onrender.com/telemetry/station-install" \
            2>/dev/null || true
    fi
fi

echo -e "${GREEN}✅ Installed.${NC}  LOCAL · BYOK · DRY-RUN · NO SENDS — everything runs on 127.0.0.1, nothing fires without your approval."
echo -e "${CYAN}================================================================${NC}"
if [ -n "$SHELL_CONFIG" ]; then
    echo -e "${CYAN}  IMPORTANT — one step so the ${NC}${GREEN}railcall${NC}${CYAN} command is found in NEW terminals:${NC}"
    echo -e "${CYAN}     • Close this terminal and open a fresh one, OR${NC}"
    echo -e "${CYAN}     • Run: source $SHELL_CONFIG${NC}"
    echo -e "${BLUE}     (Git Bash / MINGW users: this writes to ~/.bashrc so it survives session close)${NC}"
else
    echo -e "${CYAN}  IMPORTANT — no shell rc file was found, so PATH was NOT changed.${NC}"
    echo -e "${CYAN}  Paste this into your terminal now (and into your shell startup file to keep it):${NC}"
    echo -e "${GREEN}     export PATH=\"\$PATH:$RC_BIN\"${NC}"
fi
echo -e "${CYAN}================================================================${NC}"
echo -e "${GREEN}Then run:${NC}"
echo -e "${CYAN}   railcall studio${NC}  — open the visual Studio in your browser (127.0.0.1:8799)"
echo -e "${CYAN}   railcall${NC}         — the terminal dashboard (key, flows, commands)"
echo
echo -e "${GREEN}Recommended — connect your free RailCall account:${NC}"
echo -e "${CYAN}   railcall market login${NC}"
echo -e "${BLUE}   Links this station to your account so marketplace purchases and module${NC}"
echo -e "${BLUE}   licenses activate here automatically.${NC}"
echo -e "${BLUE}   No account yet? Create one free at https://railcall.ai/marketplace/signup${NC}"

# ── Stale Studio guard ───────────────────────────────────────────────────────
# An install REPLACES the files a running Studio loaded at boot. Python does not
# reload them, so the process keeps serving the OLD backend while the browser —
# which fetches static assets fresh — loads the NEW frontend. New UI, old API,
# same machine.
#
# 2026-08-22: a teammate hit exactly this after updating to v1.5.1. The new
# Modules tab asked the old backend for per-command governance, got nothing, and
# rendered "declared ?" with no controls. It looked like a missing feature, not
# a stale process, and cost an afternoon of debugging a station that was fine.
#
# The remedy string already existed in railcall_cli.py — it just never fired at
# the moment it was needed. We print, we do not kill: someone else's running
# process is not ours to terminate without being asked.
#
# Every substitution below ends in `|| true`, and that is load-bearing: this
# script runs under `set -euo pipefail`, where pgrep/grep finding NOTHING exits
# 1 and takes the whole installer down with it. Without the guards, "no Studio
# running" — the normal case on a fresh install — would abort the script at the
# final step, after a successful install, with no message. Caught in a clean
# container before shipping; on this laptop a Studio was always running, so the
# failing path never executed.
_studio_pids=""
if command -v pgrep >/dev/null 2>&1; then
    _studio_pids="$(pgrep -f 'studio_server\.py' 2>/dev/null | tr '\n' ' ' || true)"
elif command -v ps >/dev/null 2>&1; then
    # Git Bash / minimal images may lack pgrep. The [s] trick keeps grep itself
    # out of its own results.
    _studio_pids="$(ps aux 2>/dev/null | grep '[s]tudio_server\.py' | awk '{print $2}' | tr '\n' ' ' || true)"
fi
# No pgrep AND no ps: we cannot tell. Stay silent rather than warn on a guess.
_studio_pids="$(printf '%s' "$_studio_pids" | sed 's/[[:space:]]*$//' || true)"

if [ -n "$_studio_pids" ]; then
    echo
    echo -e "${YELLOW}================================================================${NC}"
    echo -e "${YELLOW}  ⚠  A RailCall Studio is ALREADY RUNNING (pid: $_studio_pids)${NC}"
    echo -e "${YELLOW}     It is now STALE — it loaded the previous version into memory${NC}"
    echo -e "${YELLOW}     and will keep serving it until restarted. Your browser will${NC}"
    echo -e "${YELLOW}     load the NEW interface against that OLD backend, which shows${NC}"
    echo -e "${YELLOW}     up as missing or broken features rather than an error.${NC}"
    echo
    echo -e "${YELLOW}     Restart it before you use Studio:${NC}"
    echo -e "${GREEN}         pkill -f studio_server.py && railcall studio${NC}"
    echo -e "${YELLOW}     Then hard-refresh the browser (Cmd/Ctrl + Shift + R).${NC}"
    echo -e "${YELLOW}================================================================${NC}"
fi
