#!/usr/bin/env bash
# Tokenmin installer.
#
# Quick install:
#   curl --proto '=https' --tlsv1.2 -fsSL https://tokenmin.ai/install.sh | bash
#
# Audit-first install — verify the script before executing:
#   curl --proto '=https' --tlsv1.2 -fsSL -o install.sh https://tokenmin.ai/install.sh
#   curl --proto '=https' --tlsv1.2 -fsSL -o install.sh.sha256 https://tokenmin.ai/install.sh.sha256
#   shasum -a 256 -c install.sh.sha256
#   less install.sh
#   bash install.sh
#
# Configuration env vars:
#   TOKENMIN_HOME           install dir          (default $HOME/.tokenmin)
#   TOKENMIN_BIN_DIR        PATH symlink dir     (default $HOME/.local/bin)
#   TOKENMIN_NO_PATH_PATCH=1  skip the shell-rc autopatch prompt
#   TOKENMIN_VERBOSE=1      show every step (quiet by default)

set -euo pipefail

DEST="${TOKENMIN_HOME:-$HOME/.tokenmin}"
BIN_DIR="${TOKENMIN_BIN_DIR:-$HOME/.local/bin}"
NO_PATCH="${TOKENMIN_NO_PATH_PATCH:-0}"
VERBOSE="${TOKENMIN_VERBOSE:-0}"

REPO="watsonrm/tokenmin-scanner"

# ---- output helpers --------------------------------------------------------
# say / ok: verbose-only — silent by default, visible with TOKENMIN_VERBOSE=1
# tell: always shown — reserved for the final success line
# warn / die: always shown — real problems
say()  { [ "${VERBOSE}" = "1" ] && printf "tokenmin: %s\n" "$*" >&2 || true; }
ok()   { [ "${VERBOSE}" = "1" ] && printf "tokenmin: \033[32m\xE2\x9C\x93\033[0m %s\n" "$*" >&2 || true; }
tell() { printf "%s\n" "$*" >&2; }
warn() { printf "tokenmin: \033[33m!\033[0m %s\n" "$*" >&2; }
die()  { printf "tokenmin: \033[31merror\033[0m %s\n" "$*" >&2; exit 1; }

# ---- input validation ------------------------------------------------------
# Refuse weird install paths (defense against malicious shell rc setting
# TOKENMIN_HOME=/etc/cron.d).
case "${DEST}" in
  "$HOME"/*|/usr/local/*|/opt/*) ;;
  *) die "refusing install path ${DEST} (must be under \$HOME, /usr/local, or /opt)";;
esac
case "${BIN_DIR}" in
  "$HOME"/*|/usr/local/*|/opt/*) ;;
  *) die "refusing symlink dir ${BIN_DIR} (must be under \$HOME, /usr/local, or /opt)";;
esac

# ---- prerequisites ---------------------------------------------------------
command -v python3 >/dev/null 2>&1 || die "python3 is required (3.10+).
  macOS:  brew install python   (or download from python.org)
  Linux:  sudo apt install python3   (or 'sudo dnf install python3')"
command -v git >/dev/null 2>&1 || die "git is required.
  macOS:  xcode-select --install   (or 'brew install git')
  Linux:  sudo apt install git"

PY_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
case "${PY_VERSION}" in
  3.1[0-9]|3.[2-9][0-9]) ;;
  *) die "python3 ${PY_VERSION} is too old; need 3.10+";;
esac

# ---- clone or update -------------------------------------------------------
if [ -d "${DEST}/.git" ]; then
  current_remote=$(git -C "${DEST}" remote get-url origin 2>/dev/null || echo "")
  if [[ "${current_remote}" != *"${REPO}"* ]]; then
    die "existing install at ${DEST} points at a different repo:
    ${current_remote}
  remove it or set TOKENMIN_HOME=<new path> and retry."
  fi
  say "updating existing install at ${DEST}"
  before=$(git -C "${DEST}" rev-parse HEAD 2>/dev/null || echo "")
  git -C "${DEST}" fetch --quiet origin 2>/dev/null || die "fetch failed (offline?)."
  git -C "${DEST}" pull --ff-only --quiet 2>/dev/null || warn "pull failed (local changes?)"
  after=$(git -C "${DEST}" rev-parse HEAD 2>/dev/null || echo "")
  if [ "${before}" = "${after}" ]; then
    ok "already up to date (${after:0:7})"
  else
    ok "updated ${before:0:7} -> ${after:0:7}"
  fi
else
  say "cloning ${REPO} into ${DEST}"
  git clone --quiet "https://github.com/${REPO}.git" "${DEST}" \
    || die "clone failed (network?)."
  ok "installed at ${DEST}"
fi

chmod +x "${DEST}/tokenmin"

# ---- symlink with conflict detection ---------------------------------------
mkdir -p "${BIN_DIR}"
link="${BIN_DIR}/tokenmin"
if [ -L "${link}" ]; then
  current_target=$(readlink "${link}")
  expected="${DEST}/tokenmin"
  if [ "${current_target}" != "${expected}" ]; then
    warn "${link} -> ${current_target} (not us)"
    if [ -t 0 ] && [ -t 2 ]; then
      printf "tokenmin: overwrite? [y/N] " >&2
      read -r ans < /dev/tty || ans=""
      case "${ans}" in y|Y|yes|YES) ;; *) die "aborted; existing symlink left alone";; esac
    else
      die "non-interactive; remove ${link} manually or set TOKENMIN_BIN_DIR to another path"
    fi
  fi
elif [ -e "${link}" ]; then
  die "${link} exists and is not a symlink we own.
  remove it or set TOKENMIN_BIN_DIR=<different-path>"
fi
ln -sf "${DEST}/tokenmin" "${link}"
ok "symlinked ${link}"

# ---- PATH check + autopatch -----------------------------------------------
patched=0
on_path=0
case ":${PATH}:" in
  *":${BIN_DIR}:"*) on_path=1;;
esac

if [ "${on_path}" = "1" ]; then
  ok "${BIN_DIR} is on PATH"
elif [ "${NO_PATCH}" = "1" ]; then
  warn "${BIN_DIR} is not on PATH (TOKENMIN_NO_PATH_PATCH=1)"
else
  # Detect shell + rc file.
  shell_name=$(basename "${SHELL:-/bin/bash}")
  rc=""
  case "${shell_name}" in
    bash) [ -f "$HOME/.bashrc" ] && rc="$HOME/.bashrc" || rc="$HOME/.bash_profile";;
    zsh)  rc="$HOME/.zshrc";;
    fish) rc="$HOME/.config/fish/config.fish";;
    *) rc="";;
  esac

  if [ -n "${rc}" ]; then
    if [ "${shell_name}" = "fish" ]; then
      line="set -gx PATH ${BIN_DIR} \$PATH"
    else
      line="export PATH=\"${BIN_DIR}:\$PATH\""
    fi

    if [ -f "${rc}" ] && grep -qF "${BIN_DIR}" "${rc}" 2>/dev/null; then
      ok "${BIN_DIR} already referenced in ${rc}"
    elif [ -t 0 ] && [ -t 2 ]; then
      printf "\ntokenmin: %s is not on PATH.\n" "${BIN_DIR}" >&2
      printf "tokenmin: append to %s?\n" "${rc}" >&2
      printf "  > %s\n" "${line}" >&2
      printf "tokenmin: [y/N] " >&2
      read -r ans < /dev/tty || ans=""
      case "${ans}" in
        y|Y|yes|YES)
          mkdir -p "$(dirname "${rc}")"
          printf "\n# Added by tokenmin installer on %s\n%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${line}" >> "${rc}"
          ok "appended to ${rc} — restart your shell or 'source ${rc}'"
          patched=1
          ;;
        *) warn "skipped. add manually: ${line}";;
      esac
    else
      warn "non-interactive; add manually to ${rc}:"
      warn "  ${line}"
    fi
  else
    warn "couldn't detect your shell rc; add to your shell config:"
    warn "  export PATH=\"${BIN_DIR}:\$PATH\""
  fi
fi

# ---- multi-Claude detection -----------------------------------------------
# Tokenmin supports every Claude variant: Code (native), Desktop (via export),
# claude.ai web (via export). Detect what's installed and tell the user what
# they can do without making them figure it out.

CLAUDE_CODE_DIR="$HOME/.claude"
case "$(uname -s 2>/dev/null)" in
  Darwin)  CLAUDE_DESKTOP_DIR="$HOME/Library/Application Support/Claude" ;;
  Linux)   CLAUDE_DESKTOP_DIR="$HOME/.config/Claude" ;;
  MINGW*|CYGWIN*|MSYS*) CLAUDE_DESKTOP_DIR="${APPDATA:-$HOME/AppData/Roaming}/Claude" ;;
  *)       CLAUDE_DESKTOP_DIR="" ;;
esac

found_code=0
found_desktop=0
say ""
say "Claude variants on this machine:"

if [ -d "${CLAUDE_CODE_DIR}" ]; then
  n_sess=0
  if [ -d "${CLAUDE_CODE_DIR}/projects" ]; then
    n_sess=$(find "${CLAUDE_CODE_DIR}/projects" -name '*.jsonl' -type f 2>/dev/null | wc -l | tr -d ' ')
  fi
  ok "Claude Code        ${CLAUDE_CODE_DIR} (${n_sess} session files)"
  found_code=1
else
  say "  Claude Code        not found at ${CLAUDE_CODE_DIR}"
fi

if [ -n "${CLAUDE_DESKTOP_DIR}" ] && [ -d "${CLAUDE_DESKTOP_DIR}" ]; then
  ok "Claude Desktop     ${CLAUDE_DESKTOP_DIR}"
  found_desktop=1
else
  if [ -n "${CLAUDE_DESKTOP_DIR}" ]; then
    say "  Claude Desktop     not found at ${CLAUDE_DESKTOP_DIR}"
  fi
fi

if [ "${found_code}" = "0" ] && [ "${found_desktop}" = "0" ]; then
  warn "no Claude install detected on this machine."
  say "  Claude Code:    https://claude.com/code"
  say "  Claude Desktop: https://claude.ai/download"
  say "  claude.ai web:  no install \xE2\x80\x94 export chats then use --source export"
fi

# ---- Claude Code plugin registration --------------------------------------
# Without this step, typing "tokenmin" inside an interactive Claude Code session
# is just a word; Claude treats it as a topic and asks what you want. Registering
# the bundled .claude-plugin marketplace makes "tokenmin" a real trigger phrase
# (the skill description routes it to a CLI run + summary). Idempotent.
plugin_registered=0
if [ "${found_code}" = "1" ] && command -v claude >/dev/null 2>&1; then
  if claude plugin list 2>/dev/null | grep -qE 'tokenmin@tokenmin$'; then
    ok "Claude Code plugin already registered"
    plugin_registered=1
  else
    say "registering tokenmin as a Claude Code plugin (so 'tokenmin' triggers inline)"
    mp_ok=0
    if claude plugin marketplace list 2>/dev/null | grep -qE 'tokenmin$'; then
      mp_ok=1
    elif claude plugin marketplace add "${DEST}" >/dev/null 2>&1; then
      mp_ok=1
    fi
    if [ "${mp_ok}" = "1" ] && claude plugin install "tokenmin@tokenmin" --scope user >/dev/null 2>&1; then
      ok "Claude Code plugin installed — type 'tokenmin' inside a session to run"
      plugin_registered=1
    else
      warn "couldn't auto-register Claude Code plugin; do it once by hand inside a Claude Code session:"
      warn "  /plugin marketplace add ${DEST}"
      warn "  /plugin install tokenmin@tokenmin"
    fi
  fi
elif [ "${found_code}" = "1" ]; then
  say ""
  say "Claude Code is installed, but the 'claude' CLI isn't on PATH."
  say "To enable inline 'tokenmin' inside a Claude Code session, run once:"
  say "  /plugin marketplace add ${DEST}"
  say "  /plugin install tokenmin@tokenmin"
fi

# ---- greet -----------------------------------------------------------------
# Quiet default: one line of "what version did I install + what to run next".
# Verbose mode (TOKENMIN_VERBOSE=1) keeps the multi-block tip set below.
VERSION_STR=""
if [ -f "${DEST}/VERSION" ]; then
  VERSION_STR=" $(cat "${DEST}/VERSION")"
fi
NEXT="tokenmin"
[ "${found_code}" = "0" ] && [ "${found_desktop}" = "0" ] && NEXT="tokenmin help-export"
tell "$(printf "tokenmin%s installed → type \033[36m%s\033[0m for your first audit" "${VERSION_STR}" "${NEXT}")"

# Verbose-only block: the multi-step tip menu the old install always printed.
say ""
ok "tokenmin installed."
say "  tokenmin --version              what you have"
say "  tokenmin doctor                 self-diagnose"
say "  tokenmin --selfcheck            see the anonymizer rules"

if [ "${found_code}" = "1" ]; then
  say ""
  say "Claude Code (the easy one — reads your sessions directly):"
  say "  tokenmin --days 7              full report"
fi

if [ "${found_desktop}" = "1" ]; then
  say ""
  say "Claude Desktop (live native parser still in progress; use export today):"
  say "  tokenmin help-export                 walk through the export, with a deep-link"
  say "  tokenmin --source export --watch-downloads   auto-run when the export arrives"
  say "  tokenmin demo                        see what a report looks like first"
fi

if [ "${found_code}" = "0" ] && [ "${found_desktop}" = "0" ]; then
  say ""
  say "Once you've used a Claude product on this machine, return and run:"
  say "  tokenmin --days 7"
  say ""
  say "If you use claude.ai (web) only:"
  say "  tokenmin help-export                 step-by-step export instructions"
  say "  tokenmin demo                        see what a report looks like first"
fi

# Restart-shell reminder is always shown when the PATH autopatch fired —
# it's actionable, not decorative.
if [ "${patched}" = "1" ]; then
  warn "restart your shell or 'source ${rc}' to pick up the new PATH"
fi
