#!/bin/sh
# Simantic CLI installer.
#
#   curl -fsSL https://simantic.com/install | sh
#
# Downloads the `sim` binary named by the release manifest, verifies its
# published sha256, installs it to ~/.local/bin, and — when the shell is
# interactive — hands off to `sim login` so browser sign-in happens right here
# instead of as a separate step the user has to discover.
#
# The releases bucket is private. Every download goes through the `get-release`
# edge function, which trades a Simantic token for a 5-minute signed URL. The
# token is SIMANTIC_TOKEN if set, else the one `sim login` saved in ~/.sim_id,
# else one obtained here through the same device login as `sim login` (and saved
# to ~/.sim_id in the same format, so the user is not asked to sign in twice).
#
# Contract: this script reads releases/pyrite/latest.json via get-release. That
# manifest is written by pyrite's .github/workflows/release-engine-rust.yml:
# {version, artifacts: {<key>: {path, sha256}}}, where path is the bare object
# path in the releases bucket and the `sim` executable for a platform is the
# artifact `sim-<rid>` (a raw binary; the same manifest also carries
# engine-rust-<rid> wheels). Change it in both places or this installer breaks.
#
# The whole body lives in main(), invoked on the last line, so a download that
# is truncated mid-flight can never execute a partial script.

set -eu

API_BASE="${SIMANTIC_API_BASE:-https://drjdhqfvrttolueolzif.supabase.co/functions/v1}"
CREDS_FILE="$HOME/.sim_id"
TOKEN="${SIMANTIC_TOKEN:-}"
SIGNED_IN=false
BIN_NAME="sim"
BIN_DIR="${SIMANTIC_BIN_DIR:-$HOME/.local/bin}"

PRODUCT="pyrite"
ARTIFACT_KEY=""

VERSION=""
CHECK_ONLY=false
FORCE=false
QUIET=false
DO_LOGIN=false
ALLOW_UNVERIFIED=false

usage() {
    cat <<EOF
Usage: install.sh [OPTIONS]

Install the Simantic CLI (\`sim\`, from pyrite).

Options:
  --version X.Y.Z      Install a specific version (default: latest)
  --bin-dir DIR        Install location (default: \$HOME/.local/bin)
  --check              Print the latest version and exit
  --force              Reinstall even if the same version is present
  --login              Sign in after install (default: no; downloading already signs in)
  --allow-unverified   Permit a pinned --version with no published checksum
  --quiet              Suppress informational output
  -h, --help           Show this help

Environment:
  SIMANTIC_BIN_DIR         Same as --bin-dir
  SIMANTIC_TOKEN           Simantic token (smtc_...); skips the browser sign-in
  SIMANTIC_API_BASE        Override the edge-function base URL (testing)
EOF
}

# ── output ──────────────────────────────────────────────────────────────
_use_color=true
[ -n "${NO_COLOR:-}" ] && _use_color=false
[ -t 2 ] || _use_color=false
if [ "$_use_color" = true ]; then
    _red='\033[31m'; _yel='\033[33m'; _grn='\033[32m'; _bld='\033[1m'; _rst='\033[0m'
else
    _red=''; _yel=''; _grn=''; _bld=''; _rst=''
fi

say()  { [ "$QUIET" = true ] || printf '%s\n' "$*"; }
ok()   { [ "$QUIET" = true ] || printf '%b%s%b %s\n' "$_grn" "ok:" "$_rst" "$*"; }
warn() { printf '%bwarn:%b %s\n' "$_yel" "$_rst" "$*" >&2; }
die()  { printf '%berror:%b %s\n' "$_red" "$_rst" "$*" >&2; exit 1; }

need() {
    command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

# Every network fetch goes through here: --proto '=https' refuses any redirect
# that would leave TLS (including to file://), so a tampered manifest cannot
# downgrade the transport.
fetch() {
    curl --proto '=https' --tlsv1.2 -fsSL "$@"
}

# ── authentication ──────────────────────────────────────────────────────
# POST {} (or $2) to an edge function and print the body.
post_json() {
    fetch -X POST -H 'Content-Type: application/json' -d "${2:-{\}}" "$1"
}

# Device login, same endpoints as `sim login`. Sets TOKEN and writes
# ~/.sim_id (EMAIL=/API_KEY=, mode 600) exactly as the CLI does.
device_login() {
    start="$(post_json "$API_BASE/cli-login-start")" || die "could not start sign-in"
    device="$(json_string "$start" device_code)"
    user_code="$(json_string "$start" user_code)"
    verify="$(json_string "$start" verify_url)"
    interval=3
    [ -n "$device" ] && [ -n "$user_code" ] && [ -n "$verify" ] || die "sign-in service returned an incomplete response"

    printf '\nSign in to download Simantic.\n  One-time code: %s\n  Open to confirm: %s\n\nWaiting for approval...\n' \
        "$user_code" "$verify" >&2

    waited=0
    while [ "$waited" -lt 600 ]; do
        sleep "$interval"
        waited=$((waited + interval))
        resp="$(post_json "$API_BASE/cli-login-poll" "{\"device_code\":\"$device\"}")" || continue
        case "$(json_string "$resp" status)" in
            pending) ;;
            approved)
                TOKEN="$(json_string "$resp" token)"
                [ -n "$TOKEN" ] || die "sign-in was approved without a token"
                email="$(json_string "$resp" email)"
                if [ -n "$email" ]; then
                    (umask 077; printf 'EMAIL=%s\nAPI_KEY=%s\n' "$email" "$TOKEN" > "$CREDS_FILE")
                    SIGNED_IN=true
                    ok "Signed in as $email"
                fi
                return 0 ;;
            denied)   die "sign-in was denied in the browser" ;;
            expired)  die "the sign-in code expired; run the installer again" ;;
            consumed) die "this sign-in code was already used; run the installer again" ;;
        esac
    done
    die "timed out waiting for sign-in"
}

ensure_token() {
    [ -z "$TOKEN" ] || return 0
    if [ -r "$CREDS_FILE" ]; then
        TOKEN="$(sed -n 's/^API_KEY=//p' "$CREDS_FILE" | head -1)"
        [ -z "$TOKEN" ] || { SIGNED_IN=true; return 0; }
    fi
    device_login
}

# Print a signed URL for release object path $1 (get-release, 300s lifetime).
signed_url() {
    out="$(curl --proto '=https' --tlsv1.2 -sS -w '\n%{http_code}' \
        -H "Authorization: Bearer $TOKEN" "$API_BASE/get-release?path=$1")" \
        || die "could not reach the release service"
    code="$(printf '%s\n' "$out" | tail -n1)"
    body="$(printf '%s\n' "$out" | sed '$d')"
    case "$code" in
        200) ;;
        401) die "Simantic token rejected. Unset SIMANTIC_TOKEN, or delete $CREDS_FILE and re-run to sign in again." ;;
        403) die "$(printf '%s' "$body" | sed -n 's/.*"error" *: *"\([^"]*\)".*/\1/p') (account not approved for downloads)" ;;
        404) die "release not found: $1" ;;
        *)   die "release service returned HTTP $code" ;;
    esac
    _u="$(json_string "$body" url)"
    [ -n "$_u" ] || die "release service returned no URL"
    printf '%s' "$_u"
}

# Release object paths come from the manifest (or --version): reject anything
# that could escape the bucket layout.
valid_path() {
    case "$1" in
        ""|*..*|/*|*:*|*"?"*|*"&"*|*"#"*|*" "*) return 1 ;;
    esac
}

# Fetch release object path $1 through get-release; remaining args go to curl.
fetch_release() {
    valid_path "$1" || die "refusing invalid release path: $1"
    _path="$1"; shift
    _signed="$(signed_url "$_path")"
    fetch "$_signed" "$@"
}

# ── platform ────────────────────────────────────────────────────────────
# Maps to the RIDs actually published in the manifest. Unpublished platforms
# fail loudly rather than downloading something that will not run.
detect_rid() {
    _os="$(uname -s)"
    _arch="$(uname -m)"
    case "$_os" in
        Linux)
            case "$_arch" in
                x86_64|amd64) printf 'linux-x64' ;;
                *) die "no published build for Linux $_arch (available: x86_64)" ;;
            esac
            ;;
        Darwin)
            case "$_arch" in
                arm64|aarch64) printf 'osx-arm64' ;;
                x86_64) die "no published build for Intel macOS (available: Apple Silicon)" ;;
                *) die "unsupported macOS arch: $_arch" ;;
            esac
            ;;
        MINGW*|MSYS*|CYGWIN*) die "Windows is not supported by this installer" ;;
        *) die "unsupported OS: $_os" ;;
    esac
}

# ── manifest ────────────────────────────────────────────────────────────
# The manifest is small, pretty-printed JSON. Rather than depend on jq, collapse
# whitespace and pull fields out with sed — safe here because every value we read
# (version, URL, hex digest) is whitespace-free by construction.
json_string() {
    # $1 = json text, $2 = key
    printf '%s' "$1" | tr -d '\n\r\t ' | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p"
}

json_artifact_field() {
    # $1 = json text, $2 = rid, $3 = field within that rid's object
    printf '%s' "$1" | tr -d '\n\r\t ' \
        | sed -n "s/.*\"$2\":{\([^}]*\)}.*/\1/p" \
        | sed -n "s/.*\"$3\":\"\([^\"]*\)\".*/\1/p"
}

sha256_of() {
    if command -v shasum >/dev/null 2>&1; then
        shasum -a 256 "$1" | awk '{print $1}'
    elif command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$1" | awk '{print $1}'
    else
        die "no sha256 tool found (shasum or sha256sum)"
    fi
}

# Detect `sim login` support instead of assuming it.
supports_login() {
    "$1" --help 2>/dev/null | grep -qw 'login'
}

main() {
    while [ $# -gt 0 ]; do
        case "$1" in
            --version)          VERSION="${2:?--version needs an argument}"; shift 2 ;;
            --version=*)        VERSION="${1#*=}"; shift ;;
            --bin-dir)          BIN_DIR="${2:?--bin-dir needs an argument}"; shift 2 ;;
            --bin-dir=*)        BIN_DIR="${1#*=}"; shift ;;
            --check)            CHECK_ONLY=true; shift ;;
            --force)            FORCE=true; shift ;;
            --login)            DO_LOGIN=true; shift ;;
            --no-login)         DO_LOGIN=false; shift ;;  # default; kept so existing scripts still parse
            --allow-unverified) ALLOW_UNVERIFIED=true; shift ;;
            --quiet)            QUIET=true; shift ;;
            -h|--help)          usage; exit 0 ;;
            *) printf 'install.sh: unknown option: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
        esac
    done

    need curl

    RID="$(detect_rid)"
    ARTIFACT_KEY="sim-$RID"
    say "Platform: $RID"

    if [ "$CHECK_ONLY" = true ]; then
        # Never start a device login for a read-only check: use a token only
        # if one already exists.
        if [ -z "$TOKEN" ] && [ -r "$CREDS_FILE" ]; then
            TOKEN="$(sed -n 's/^API_KEY=//p' "$CREDS_FILE" | head -1)"
        fi
        [ -n "$TOKEN" ] || die "--check needs a saved sign-in (SIMANTIC_TOKEN or ~/.sim_id); run 'sim login' first"
    else
        ensure_token
    fi

    MANIFEST_PATH="$PRODUCT/latest.json"
    say "Manifest:  $MANIFEST_PATH"
    MANIFEST="$(fetch_release "$MANIFEST_PATH")" || die "could not fetch release manifest"

    LATEST="$(json_string "$MANIFEST" version)"
    [ -n "$LATEST" ] || die "manifest has no version field"

    if [ "$CHECK_ONLY" = true ]; then
        say "Latest version: $LATEST"
        exit 0
    fi

    OBJ_PATH="$(json_artifact_field "$MANIFEST" "$ARTIFACT_KEY" path)"
    SHA="$(json_artifact_field "$MANIFEST" "$ARTIFACT_KEY" sha256)"

    if [ -n "$VERSION" ] && [ "$VERSION" != "$LATEST" ]; then
        # Only latest.json carries checksums today, so a pinned older version
        # cannot be verified. Refuse by default rather than silently dropping
        # the integrity check — publishing per-version manifests would remove
        # this restriction.
        [ "$ALLOW_UNVERIFIED" = true ] || \
            die "no published checksum for $PRODUCT $VERSION (only $LATEST is verifiable); re-run with --allow-unverified to install it anyway"
        warn "installing $VERSION without checksum verification"
        OBJ_PATH="$PRODUCT/$VERSION/$BIN_NAME-$VERSION-$RID"
        SHA=""
    else
        VERSION="$LATEST"
        [ -n "$OBJ_PATH" ] || die "manifest has no $ARTIFACT_KEY build for $PRODUCT $VERSION"
        [ -n "$SHA" ] || die "manifest has no checksum for $ARTIFACT_KEY"
    fi

    valid_path "$OBJ_PATH" || die "refusing invalid release path: $OBJ_PATH"

    EXISTING="$BIN_DIR/$BIN_NAME"
    if [ -x "$EXISTING" ] && [ "$FORCE" != true ]; then
        # `sim --version` prints e.g. 0.4.0+<commit>; compare the release
        # version only, or the check never matches and every run reinstalls.
        current="$("$EXISTING" --version 2>/dev/null | awk 'NR==1 {print $NF}' | sed 's/+.*//' || true)"
        if [ -n "$current" ] && [ "$current" = "$VERSION" ]; then
            ok "$BIN_NAME $current already installed at $EXISTING (use --force to reinstall)"
            post_install "$EXISTING"
            exit 0
        fi
    fi

    umask 077
    tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/simantic-install.XXXXXX")"
    trap 'rm -rf "$tmpdir"' EXIT INT TERM HUP

    say "Downloading $PRODUCT v$VERSION ($RID)"
    fetch_release "$OBJ_PATH" -o "$tmpdir/pkg" || die "download failed: $OBJ_PATH"

    if [ -n "$SHA" ]; then
        got="$(sha256_of "$tmpdir/pkg")"
        [ "$got" = "$SHA" ] || die "sha256 mismatch (expected $SHA, got $got)"
        ok "sha256 verified"
    fi

    SRC="$tmpdir/pkg"

    mkdir -p "$BIN_DIR"
    # Install via a temp name + rename so the swap is atomic and never hits
    # ETXTBSY against a running process.
    cp "$SRC" "$BIN_DIR/$BIN_NAME.new"
    chmod 755 "$BIN_DIR/$BIN_NAME.new"
    # A plain curl never sets com.apple.quarantine, so Gatekeeper has
    # nothing to check here — but some corporate proxies/EDR agents tag
    # downloaded files regardless of the fetching tool. Strip it if present
    # so a signed-but-tagged binary doesn't hit "unidentified developer".
    if [ "$(uname -s)" = "Darwin" ] && command -v xattr >/dev/null 2>&1; then
        xattr -d com.apple.quarantine "$BIN_DIR/$BIN_NAME.new" 2>/dev/null || true
    fi
    mv "$BIN_DIR/$BIN_NAME.new" "$BIN_DIR/$BIN_NAME"
    ok "Installed $BIN_NAME $VERSION to $BIN_DIR/$BIN_NAME"

    case ":${PATH:-}:" in
        *":$BIN_DIR:"*) ;;
        *)
            say ""
            say "$BIN_DIR is not on your PATH. Add to your shell profile:"
            # shellcheck disable=SC2016  # $PATH is meant to stay literal here
            printf '\n    %bexport PATH="%s:$PATH"%b\n\n' "$_bld" "$BIN_DIR" "$_rst"
            ;;
    esac

    post_install "$BIN_DIR/$BIN_NAME"
}

# Hand off to browser sign-in while the user is still here.
#
# `curl … | sh` leaves stdin bound to the script pipe, so an interactive command
# would read script bytes instead of user input — the sign-in is explicitly
# reconnected to /dev/tty. Non-interactive shells (CI) print the command instead
# of hanging, and a failed sign-in never fails the install.
post_install() {
    post_install_bin="$1"
    [ "$DO_LOGIN" = true ] || return 0
    [ "$SIGNED_IN" != true ] || return 0   # already signed in; ~/.sim_id is in place

    if [ ! -t 1 ] || [ ! -r /dev/tty ]; then
        say ""
        say "Non-interactive shell — skipping sign-in. Run: $BIN_NAME login"
        return 0
    fi

    if supports_login "$post_install_bin"; then
        say ""
        say "Signing in…"
        "$post_install_bin" login < /dev/tty \
            || warn "sign-in did not complete — run '$BIN_NAME login' to retry"
    else
        say ""
        say "This build has no browser sign-in yet. Authenticate with:"
        say "    $BIN_NAME auth"
    fi
}

main "$@"
