#!/bin/sh
# Simetrik CLI installer (macOS / Linux)
# Usage:
#   curl -fsSL https://cli.simetrik.com | sh
#   curl -fsSL https://cli.simetrik.com | sh -s -- --version 0.7.6 --install-dir /usr/local/bin
set -eu

CDN_BASE="${SIMETRIK_CDN_BASE:-https://cli.simetrik.com}"
INSTALL_DIR="${SIMETRIK_INSTALL_DIR:-$HOME/.simetrik/bin}"
VERSION="${SIMETRIK_VERSION:-}"
CHANNEL="${SIMETRIK_CHANNEL:-stable}"
MODIFY_PATH=1
FORCE=0
QUIET=0
RUN_DOCTOR=1
SKIP_SKILL=0

# --- Parse flags ---
while [ $# -gt 0 ]; do
  case "$1" in
    --version)        VERSION="$2"; shift 2 ;;
    --channel)        CHANNEL="$2"; shift 2 ;;
    --install-dir)    INSTALL_DIR="$2"; shift 2 ;;
    --no-modify-path) MODIFY_PATH=0; shift ;;
    --skip-doctor)    RUN_DOCTOR=0; shift ;;
    --skip-skill)     SKIP_SKILL=1; shift ;;
    --force)          FORCE=1; shift ;;
    --quiet)          QUIET=1; shift ;;
    -h|--help)
      cat <<EOF
simetrik installer

Flags:
  --version <X>      Install a specific version (default: latest stable)
  --channel <name>   Release channel: stable | beta (default: stable)
  --install-dir <p>  Install directory (default: \$HOME/.simetrik/bin)
  --no-modify-path   Skip appending PATH to your shell rc
  --skip-doctor      Skip the post-install \`simetrik doctor\` diagnostic
  --skip-skill       Skip installing the Claude Code skill bundle
  --force            Overwrite existing binary without prompting
  --quiet            Suppress non-error output

Env vars: SIMETRIK_CDN_BASE, SIMETRIK_INSTALL_DIR, SIMETRIK_VERSION, SIMETRIK_CHANNEL
EOF
      exit 0
      ;;
    *) echo "unknown flag: $1" >&2; exit 1 ;;
  esac
done

# --- CI detection: turn off interactive behavior ---
if [ ! -t 1 ] || [ "${CI:-}" = "true" ] || [ "$QUIET" = "1" ]; then
  MODIFY_PATH=0
  RUN_DOCTOR=0
fi

# --- Colors ---
if [ -t 1 ] && [ "$QUIET" = "0" ]; then
  BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; RED='\033[0;31m'; RESET='\033[0m'
else
  BOLD=''; GREEN=''; YELLOW=''; RED=''; RESET=''
fi

info() { [ "$QUIET" = "0" ] && printf "${BOLD}%s${RESET}\n" "$1"; return 0; }
ok()   { [ "$QUIET" = "0" ] && printf "${GREEN}%s${RESET}\n" "$1"; return 0; }
warn() { printf "${YELLOW}%s${RESET}\n" "$1" >&2; }
err()  { printf "${RED}error: %s${RESET}\n" "$1" >&2; exit 1; }

# --- Dependencies ---
command -v curl >/dev/null 2>&1 || err "curl is required"
command -v tar  >/dev/null 2>&1 || err "tar is required"
command -v python3 >/dev/null 2>&1 || err "python3 is required (used to parse the release manifest)"
command -v shasum >/dev/null 2>&1 || command -v sha256sum >/dev/null 2>&1 \
  || err "shasum or sha256sum is required"

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

# Lowercase a hex digest so manifests can publish either case
# (the schema accepts /^[a-f0-9]{64}$/i but our computed hash is always lc).
to_lower() {
  printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
}

# Read a JSON path from the manifest. Robust to whitespace / line-breaks /
# minification — uses the Python stdlib parser, not awk on JSON.
# Usage: manifest_get <manifest.json> <key.path.dotted>
manifest_get() {
  python3 -c '
import json, sys
try:
    obj = json.load(open(sys.argv[1]))
    for key in sys.argv[2].split("."):
        obj = obj[key]
    print(obj)
except (KeyError, TypeError):
    sys.exit(0)
' "$1" "$2"
}

# --- Detect platform ---
OS="$(uname -s)"; ARCH="$(uname -m)"
case "$OS" in
  Linux)
    case "$ARCH" in
      x86_64) PLATFORM="linux-x86_64" ;;
      *)      err "Unsupported Linux architecture: $ARCH" ;;
    esac ;;
  Darwin)
    case "$ARCH" in
      arm64)  PLATFORM="darwin-arm64" ;;
      x86_64) PLATFORM="darwin-arm64"; warn "Intel Mac detected — using ARM binary via Rosetta 2." ;;
      *)      err "Unsupported macOS architecture: $ARCH" ;;
    esac ;;
  *) err "Unsupported OS: $OS. For Windows use install.ps1." ;;
esac
info "platform: $OS $ARCH → $PLATFORM"

# --- Fetch manifest unless --version supplied ---
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

if [ -z "$VERSION" ]; then
  info "fetching $CDN_BASE/cli/manifest.json …"
  curl -fsSL -o "$TMP_DIR/manifest.json" "$CDN_BASE/cli/manifest.json" \
    || err "could not fetch manifest from $CDN_BASE — set --version <X> to bypass"
  VERSION="$(manifest_get "$TMP_DIR/manifest.json" "channels.${CHANNEL}.latest_version")"
  [ -n "$VERSION" ] || err "could not read channels.${CHANNEL}.latest_version from manifest"
fi
info "version: v$VERSION"

# --- Resolve sha256 from manifest ---
EXPECTED_SHA=""
if [ -f "$TMP_DIR/manifest.json" ]; then
  EXPECTED_SHA="$(manifest_get "$TMP_DIR/manifest.json" "channels.${CHANNEL}.platforms.${PLATFORM}.sha256")"
fi

# --- Download artifact ---
ARCHIVE="simetrik-${PLATFORM}.tar.gz"
URL="$CDN_BASE/cli/v${VERSION}/${ARCHIVE}"
info "downloading $URL …"
curl -fsSL -o "$TMP_DIR/$ARCHIVE" "$URL" || err "download failed: $URL"

# --- Verify sha256 ---
if [ -n "$EXPECTED_SHA" ]; then
  ACTUAL_SHA="$(sha256 "$TMP_DIR/$ARCHIVE")"
  if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
    rm -f "$TMP_DIR/$ARCHIVE"
    err "sha256 mismatch — expected $EXPECTED_SHA, got $ACTUAL_SHA"
  fi
  ok "sha256 verified"
else
  warn "no sha256 in manifest — skipping verification"
fi

# --- Install ---
mkdir -p "$INSTALL_DIR"
tar -xzf "$TMP_DIR/$ARCHIVE" -C "$TMP_DIR"
SRC="$TMP_DIR/simetrik-${PLATFORM}"
DEST="$INSTALL_DIR/simetrik"
if [ -e "$DEST" ] && [ "$FORCE" = "0" ]; then
  : # overwrite; ask is impossible under curl|sh, so we just proceed (matches rustup behavior)
fi
mv "$SRC" "$DEST"
chmod +x "$DEST"

# --- macOS: strip quarantine flag (best-effort) ---
if [ "$OS" = "Darwin" ] && command -v xattr >/dev/null 2>&1; then
  xattr -d com.apple.quarantine "$DEST" 2>/dev/null || true
fi

ok "installed $DEST (v$VERSION)"

# --- PATH: auto-append, idempotent ---
on_path=0
case ":$PATH:" in *":$INSTALL_DIR:"*) on_path=1 ;; esac
if [ "$on_path" = "0" ] && [ "$MODIFY_PATH" = "1" ]; then
  MARKER='# added by simetrik installer'
  RC=""
  case "${SHELL:-}" in
    */zsh)  RC="$HOME/.zshrc";  LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
    */bash) RC="$HOME/.bashrc"; LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
    */fish) RC="$HOME/.config/fish/config.fish"; LINE="fish_add_path $INSTALL_DIR" ;;
    *)      RC="$HOME/.profile"; LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
  esac
  mkdir -p "$(dirname "$RC")"
  if ! grep -F "$MARKER" "$RC" >/dev/null 2>&1; then
    printf "\n%s\n%s\n" "$MARKER" "$LINE" >> "$RC"
    ok "added PATH export to $RC"
  fi
elif [ "$on_path" = "0" ]; then
  warn "PATH not modified. Add this line to your shell rc:"
  echo "  export PATH=\"$INSTALL_DIR:\$PATH\""
fi

# --- Smoke test ---
"$DEST" --version >/dev/null 2>&1 && ok "verified: $($DEST --version)"

# --- Optional: Claude Code skill bundle ---
# Best-effort: if Claude Code is detected on this machine, download the
# matching skill bundle from the same CDN/manifest entry as the binary and
# install it to ~/.claude/skills/simetrik/. The skill is platform-agnostic
# markdown — one bundle per channel, versioned in lockstep with the CLI.
# Any failure (network, sha mismatch, missing skill entry in manifest, no
# Claude Code on the box) warns and continues — the CLI install itself is
# never blocked by skill issues.
install_skill() {
  if [ "$SKIP_SKILL" = "1" ]; then
    return 0
  fi
  if [ ! -d "$HOME/.claude" ]; then
    info ""
    info "Claude Code not detected — skill skipped."
    info "→ Install Claude Code, then run 'simetrik skill install' to fetch it."
    return 0
  fi

  SKILL_FILENAME=""
  SKILL_SHA=""
  if [ -f "$TMP_DIR/manifest.json" ]; then
    SKILL_FILENAME="$(manifest_get "$TMP_DIR/manifest.json" "channels.${CHANNEL}.skill.filename")"
    SKILL_SHA="$(manifest_get "$TMP_DIR/manifest.json" "channels.${CHANNEL}.skill.sha256")"
  fi

  if [ -z "$SKILL_FILENAME" ]; then
    info "no skill published for channel '$CHANNEL' — skipping."
    return 0
  fi

  SKILL_URL="$CDN_BASE/cli/v${VERSION}/${SKILL_FILENAME}"
  info "downloading skill: $SKILL_URL …"
  if ! curl -fsSL -o "$TMP_DIR/$SKILL_FILENAME" "$SKILL_URL"; then
    warn "skill download failed — continuing without skill."
    return 0
  fi

  if [ -n "$SKILL_SHA" ]; then
    SKILL_ACTUAL="$(sha256 "$TMP_DIR/$SKILL_FILENAME")"
    SKILL_EXPECTED="$(to_lower "$SKILL_SHA")"
    if [ "$SKILL_ACTUAL" != "$SKILL_EXPECTED" ]; then
      warn "skill sha256 mismatch (expected $SKILL_SHA, got $SKILL_ACTUAL) — skipping skill."
      return 0
    fi
  fi

  # Stage-then-swap: extract into a staging dir first, then atomically replace
  # the live skill dir only after extraction succeeds. Two reasons:
  #   1. tar -xzf OVERWRITES files but does not REMOVE files absent from the
  #      new bundle, so an in-place extract leaves stale content from older
  #      releases. The swap deletes the old dir cleanly.
  #   2. If extraction fails (corrupt download, partial transfer, disk full),
  #      we must NOT have already deleted a working skill — the install block
  #      is best-effort and a failed update must leave the previous skill
  #      intact rather than wiping it.
  SKILL_DEST="$HOME/.claude/skills/simetrik"
  SKILL_STAGING="$TMP_DIR/skill-staging"
  mkdir -p "$SKILL_STAGING"
  # Let tar's stderr flow to the terminal — silencing it swallows actionable
  # diagnostics (corrupt tarball, disk full, permission denied).
  if ! tar -xzf "$TMP_DIR/$SKILL_FILENAME" -C "$SKILL_STAGING"; then
    warn "skill extraction failed — continuing (previous skill, if any, left intact)."
    return 0
  fi
  # Verify the bundle contains the expected entry point before swapping.
  # If a future release pipeline accidentally changes the tar layout (e.g.
  # `-C . skill` instead of `-C skill .`) we'd silently install a broken
  # skill — this check fails loud instead.
  if [ ! -f "$SKILL_STAGING/SKILL.md" ]; then
    warn "skill bundle is missing SKILL.md — skipping (previous skill left intact)."
    return 0
  fi
  mkdir -p "$(dirname "$SKILL_DEST")"
  rm -rf "$SKILL_DEST"
  mv "$SKILL_STAGING" "$SKILL_DEST"
  ok "installed Claude skill to $SKILL_DEST"
}

install_skill

# --- Post-install doctor ---
# Runs the subset of doctor checks that's meaningful right after install:
# binary, path, stale-cache, network, terminal, disk, proxy. Auth/config/
# updates/completion are skipped — a fresh install has no token/config yet,
# and the user is already on the latest version. `--fix` auto-heals stale
# caches left by a previous install (the cli_access.json allowed:false bug
# would otherwise block every command for up to 2h). The doctor's exit code
# is intentionally swallowed: a warn (e.g. PATH needs shell restart) must
# not abort an otherwise-successful install — the binary is already verified
# above.
if [ "$RUN_DOCTOR" = "1" ]; then
  info ""
  info "running post-install diagnostic …"
  "$DEST" doctor --post-install --fix || true
else
  info ""
  info "→ Run 'simetrik doctor' for a full environment check."
fi

# --- Shell reload reminder ---
# A child process cannot mutate the parent shell's environment (POSIX), so
# even after appending the export to the rc file the user's current shell
# still has no $INSTALL_DIR in PATH. Print a loud, copy-pasteable reminder
# so users do not hit `command not found: simetrik` on the very next prompt.
if [ "$on_path" = "0" ] && [ "$MODIFY_PATH" = "1" ] && [ -n "${RC:-}" ]; then
  printf "\n"
  printf "${YELLOW}============================================================${RESET}\n"
  printf "${BOLD}${YELLOW}  simetrik is installed but NOT yet on PATH in this shell.${RESET}\n"
  printf "\n"
  printf "  To start using it right now, run:\n"
  printf "\n"
  printf "${BOLD}      source %s${RESET}\n" "$RC"
  printf "\n"
  printf "  Or simply open a new terminal.\n"
  printf "${YELLOW}============================================================${RESET}\n"
fi
