#!/bin/bash
# set-secret — create/update secrets & folders in Infisical via the official `infisical` CLI.
# Shares the keychain bootstrap + token cache + .infisical.json project resolution with get-secret.
#
#   echo -n "$VAL" | set-secret [--env E] <folder>/<NAME> -   # value from stdin (preferred — never in argv/history)
#   set-secret [--env E] <folder>/<NAME> @<file>             # value from a file
#   set-secret [--env E] <folder>/<NAME> <value>             # only on a TTY (refused from scripts/agents)
#   set-secret [--env E] mkfolder <folder>                   # create folder (idempotent)
#   set-secret [--env E] rm <folder>/<NAME>                  # delete a secret
#
# Setup: see the notes in `get-secret`. Fill the CONFIG block, drop in PATH, chmod +x,
# and add a .infisical.json per repo.
set -euo pipefail

# --- CONFIG: edit these (auth only — project/env live in .infisical.json; keep in sync with get-secret) ---
INFISICAL_DOMAIN="https://your-infisical-instance.example.com"
KEYCHAIN_SVC="infisical-machine-identity"
# ---------------------------------------------------------------------------------------------------------
CACHE_FILE="$HOME/.cache/infisical-token"
CACHE_TTL_MIN=25   # must stay below the machine identity's access-token TTL

err() { echo "set-secret: $*" >&2; exit 1; }
command -v infisical >/dev/null 2>&1 || err "missing dependency: infisical CLI"
command -v jq >/dev/null 2>&1 || err "missing dependency: jq"

# Project + environment come from the repo's .infisical.json (see get-secret for details).
[ -f .infisical.json ] || err "no .infisical.json in $(pwd). Bind this repo to a project once:
  printf '{\"workspaceId\":\"<projectId>\",\"defaultEnvironment\":\"prod\"}' > .infisical.json"
jq -e . .infisical.json >/dev/null 2>&1 || err ".infisical.json is not valid JSON."
PROJECT_ID=$(jq -r '.workspaceId // empty' .infisical.json)
ENVIRONMENT=$(jq -r '.defaultEnvironment // empty' .infisical.json)
[ -n "$PROJECT_ID" ] || err ".infisical.json has no workspaceId."

while [ $# -gt 0 ]; do
  case "$1" in
    --env) shift; ENVIRONMENT="${1:?--env needs a value}"; shift ;;
    *) break ;;
  esac
done
[ -n "$ENVIRONMENT" ] || err "no environment — set defaultEnvironment in .infisical.json or pass --env."

get_token() {
  if [ -f "$CACHE_FILE" ] && [ -n "$(find "$CACHE_FILE" -mmin -${CACHE_TTL_MIN} 2>/dev/null)" ]; then
    cat "$CACHE_FILE"; return
  fi
  local creds cid csec tok
  creds=$(security find-generic-password -s "$KEYCHAIN_SVC" -w 2>/dev/null) \
    || err "no creds in keychain (service=$KEYCHAIN_SVC)."
  cid="${creds%%:*}"; csec="${creds##*:}"
  tok=$(infisical login --method=universal-auth --client-id="$cid" --client-secret="$csec" \
        --domain="$INFISICAL_DOMAIN" --plain --silent 2>/dev/null) || err "infisical login failed"
  [ -n "$tok" ] || err "login returned empty token"
  mkdir -p "$(dirname "$CACHE_FILE")"
  ( umask 077; printf '%s' "$tok" > "$CACHE_FILE" )   # 600 from creation, no chmod race
  printf '%s' "$tok"
}

# read value from stdin (-) / file (@path) / arg. The arg form is refused when stdin is not a
# TTY (scripts/AI agents): a value in argv leaks into the process table + shell history.
read_value() {
  local src="$1"
  if [ "$src" = "-" ]; then cat
  elif [[ "$src" == @* ]]; then local f="${src#@}"; [ -f "$f" ] || err "file not found: $f"; cat "$f"
  else
    [ -t 0 ] || err "value passed as argument from a non-interactive context — refusing (lands in argv/history). Use stdin: echo -n \"\$VAL\" | set-secret <folder>/<NAME> -  (or @<file>)."
    printf '%s' "$src"
  fi
}

cmd_set() {
  local arg="$1" src="${2:-}"
  [ -n "$src" ] || err "missing value source. Use: -, @<file>, or <value>"
  [[ "$arg" == */* ]] || err "expected <folder>/<NAME>, got: $arg"
  local folder="${arg%/*}" name="${arg##*/}" val tok
  val=$(read_value "$src"); [ -n "$val" ] || err "empty value (refusing to set)"
  tok=$(get_token)
  # stdout -> /dev/null hides the value table `secrets set` echoes; stderr stays visible so a real
  # failure (bad project, no write perm) surfaces instead of a blank "set failed".
  infisical secrets set "$name=$val" --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
    --env "$ENVIRONMENT" --path "/$folder" --type shared --silent >/dev/null \
    || err "infisical set failed for /$folder/$name"
  echo "set /$folder/$name (len=${#val})"
}

cmd_mkfolder() {
  local folder="${1:?usage: set-secret mkfolder <folder>}" tok; tok=$(get_token)
  if infisical secrets folders get --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
      --env "$ENVIRONMENT" --path / -o json --silent 2>/dev/null \
      | jq -e --arg n "$folder" '.[]? | select(.folderName==$n)' >/dev/null 2>&1; then
    echo "folder /$folder already exists"; return 0
  fi
  infisical secrets folders create --name "$folder" --path / --token "$tok" --projectId "$PROJECT_ID" \
    --domain "$INFISICAL_DOMAIN" --env "$ENVIRONMENT" --silent >/dev/null \
    || err "create folder failed: /$folder"
  echo "created folder /$folder"
}

cmd_rm() {
  local arg="${1:?usage: set-secret rm <folder>/<NAME>}"
  [[ "$arg" == */* ]] || err "expected <folder>/<NAME>"
  local folder="${arg%/*}" name="${arg##*/}" tok; tok=$(get_token)
  infisical secrets delete "$name" --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
    --env "$ENVIRONMENT" --path "/$folder" --type shared --silent >/dev/null \
    || err "delete failed: /$folder/$name"
  echo "deleted /$folder/$name"
}

case "${1:-}" in
  ""|-h|--help) echo "usage: set-secret [--env E] {<f>/<NAME> {-|@file|<value>} | mkfolder <f> | rm <f>/<NAME>}"; exit 0 ;;
  mkfolder) shift; cmd_mkfolder "$@" ;;
  rm)       shift; cmd_rm       "$@" ;;
  *) [ $# -ge 1 ] || err "missing args. See: set-secret --help"; cmd_set "$1" "${2:-}" ;;
esac
