#!/bin/bash
#
# credential-store.sh
# Cross-platform secret storage wrapper. Used by every script that previously
# called `security find-generic-password` directly so tokens can live in the
# native credential store on macOS, Windows, and Linux.
#
# Subcommands:
#   get <key>            Read secret value to stdout (empty + exit 1 if missing)
#   set <key> <value>    Store secret (overwrites if exists)
#   set <key> -          Same, but read the secret from stdin. Preferred:
#                        keeps the secret off this script's argv (visible to
#                        ps). A literal "-" value cannot be stored this way.
#   delete <key>         Remove secret
#   list                 List all keys (one per line)
#   platform             Print detected platform: macos | windows | linux | unknown
#   doctor               Check backend availability, print remediation if missing
#
# Backends:
#   macOS    -> Keychain via `security`
#   Windows  -> Credential Manager via PowerShell `CredentialManager` module
#              (Install-Module CredentialManager -Scope CurrentUser)
#   Linux    -> libsecret via `secret-tool`
#
# Returns 0 on success, 1 on missing/empty value, 2 on backend missing,
# 3 on usage error.

set -euo pipefail

detect_platform() {
  case "$(uname -s 2>/dev/null)" in
    Darwin) echo "macos" ;;
    Linux)  echo "linux" ;;
    MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
    *) [ -n "${WINDIR:-}${OS:-}" ] && echo "windows" || echo "unknown" ;;
  esac
}

PLATFORM=$(detect_platform)
CMD="${1:-}"
shift || true

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
KEYCHAIN_PY="$SCRIPT_DIR/../scripts/keychain.py"

# --- Python delegate --------------------------------------------------------
# On macOS / Linux, prefer the Python helper for deterministic behaviour:
#   - It writes BOTH -l (label) and -s (service) attributes on macOS, so items
#     are findable by either convention (personal tokens use -l, this script
#     historically used -s).
#   - It returns clean exit codes (0 ok / 1 missing / 2 backend / 4 error).
# Opt out with `KEYCHAIN_DELEGATE=0` if you need raw shell behaviour.
delegate_to_python() {
  [ "${KEYCHAIN_DELEGATE:-1}" != "0" ] || return 1
  command -v python3 >/dev/null 2>&1 || return 1
  [ -f "$KEYCHAIN_PY" ] || return 1
  case "$PLATFORM" in
    macos|linux) return 0 ;;
    *) return 1 ;;
  esac
}

# --- Helpers ----------------------------------------------------------------
ps_run() {
  # Execute a PowerShell command, suppressing profile load. Reads stdin if any.
  # Windows PowerShell (powershell.exe) is preferred over pwsh: the
  # CredentialManager binary module is built for .NET Framework and users
  # install it via `Install-Module CredentialManager` in Windows PowerShell,
  # so its module path is only guaranteed visible there.
  if command -v powershell.exe >/dev/null 2>&1; then
    powershell.exe -NoProfile -NonInteractive -Command "$1"
  elif command -v pwsh >/dev/null 2>&1; then
    pwsh -NoProfile -NonInteractive -Command "$1"
  else
    return 127
  fi
}

require_cmd() {
  local cmd="$1" hint="$2"
  if ! command -v "$cmd" >/dev/null 2>&1; then
    echo "ERR: $cmd not found. $hint" >&2
    exit 2
  fi
}

# --- Logical key -> backend key --------------------------------------------
# Callers pass a LOGICAL key ("github", "jira", "figma_pat"). The actual
# Keychain / libsecret / Credential Manager entry is named by
# prefs.global.keychainMapping, so a lookup that uses the logical key verbatim
# finds nothing whenever the two differ -  silently, because a missing entry and
# a missing mapping look identical from here. Resolve the mapping first and fall
# back to the logical key when no mapping exists (backward compatible with
# entries whose name already equals the logical key).
PREFS_FILE="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}"

# Reader order is python3 then node, and both are here on purpose. python3 is present
# on macOS and every mainstream Linux; node is guaranteed wherever this pipeline is
# installed at all, because the installer IS a node program. Windows commonly has node
# and no python3, and with a python3-only reader the mapping was skipped there in
# silence: the lookup fell back to the LOGICAL key, searched Credential Manager for a
# target that does not exist, and reported the token as missing while it sat under its
# mapped name. A missing interpreter must not look like a missing token.
resolve_key() {
  local logical="$1" mapped=""
  if [ -f "$PREFS_FILE" ]; then
    if command -v python3 >/dev/null 2>&1; then
      mapped=$(python3 -c '
import json, sys
try:
    with open(sys.argv[1], encoding="utf-8") as fh:
        prefs = json.load(fh)
except Exception:
    sys.exit(0)
name = prefs.get("global", {}).get("keychainMapping", {}).get(sys.argv[2])
if isinstance(name, str) and name.strip():
    sys.stdout.write(name.strip())
' "$PREFS_FILE" "$logical" 2>/dev/null || true)
    fi
    if [ -z "$mapped" ] && command -v node >/dev/null 2>&1; then
      mapped=$(node -e '
try {
  const prefs = JSON.parse(require("fs").readFileSync(process.argv[1], "utf-8"));
  const name = ((prefs.global || {}).keychainMapping || {})[process.argv[2]];
  if (typeof name === "string" && name.trim()) process.stdout.write(name.trim());
} catch { /* no mapping readable - caller falls back to the logical key */ }
' "$PREFS_FILE" "$logical" 2>/dev/null || true)
    fi
    if [ -z "$mapped" ] && ! command -v python3 >/dev/null 2>&1 && ! command -v node >/dev/null 2>&1; then
      echo "WARN: neither python3 nor node found; keychainMapping ignored, looking up '$logical' verbatim." >&2
    fi
  fi
  printf '%s' "${mapped:-$logical}"
}

# --- Subcommands ------------------------------------------------------------
# Append one PAT-lookup audit event. Every `get` goes through here, which is the
# point of the control: the audit trail described in the .gitignore hardening and
# managed by `/multi-agent:prune-logs` was written by audit-log.sh, but nothing
# ever called it, so the trail was always empty and the control was inert.
#
# Never fails the caller and never echoes a secret: audit-log.sh hashes the repo
# URL, logs the logical key (not the value), and is silent on error by design.
audit_lookup() {
  local logical="$1" success="$2"
  local audit="$SCRIPT_DIR/../scripts/audit-log.sh"
  [ -f "$audit" ] || return 0
  bash "$audit" pat_lookup "$logical" "${USER:-unknown}" "${MULTI_AGENT_REPO_URL:-}" "$success" \
    >/dev/null 2>&1 || true
}

do_get() {
  local key="${1:-}"
  [ -z "$key" ] && { echo "usage: $0 get <key>" >&2; exit 3; }
  local logical="$key"
  key=$(resolve_key "$key")
  if delegate_to_python; then
    local val rc
    val=$(python3 "$KEYCHAIN_PY" get "$key" 2>/dev/null) || rc=$?
    rc=${rc:-0}
    if [ "$rc" -eq 0 ] && [ -n "$val" ]; then
      audit_lookup "$logical" true
      printf '%s' "$val"
      return 0
    fi
    audit_lookup "$logical" false
    return 1
  fi
  local val=""
  case "$PLATFORM" in
    macos)
      val=$(security find-generic-password -s "$key" -w 2>/dev/null || true)
      ;;
    linux)
      require_cmd secret-tool "Install: sudo apt install libsecret-tools (Debian/Ubuntu) or sudo dnf install libsecret (Fedora)."
      val=$(secret-tool lookup service "$key" 2>/dev/null || true)
      ;;
    windows)
      # Escape single quotes for PowerShell single-quoted strings ('' = literal ').
      local key_esc="${key//\'/\'\'}"
      val=$(ps_run "
\$ErrorActionPreference='SilentlyContinue'
\$c = Get-StoredCredential -Target '$key_esc'
if (\$c) { \$c.GetNetworkCredential().Password }
" 2>/dev/null | tr -d '\r' || true)
      ;;
    *)
      echo "ERR: unsupported platform" >&2; exit 2 ;;
  esac
  if [ -z "$val" ]; then
    audit_lookup "$logical" false
    return 1
  fi
  audit_lookup "$logical" true
  printf '%s' "$val"
}

do_set() {
  local key="${1:-}" val="${2:-}"
  [ -z "$key" ] && { echo "usage: $0 set <key> <value|->" >&2; exit 3; }
  # Resolve through the same mapping as do_get so set/get stay symmetric.
  key=$(resolve_key "$key")
  # "-" means: read the secret from stdin (keeps it off argv).
  if [ "$val" = "-" ]; then
    val=$(cat)
  fi
  if delegate_to_python; then
    # Pass the value via stdin to keep it out of process listings / shell history.
    printf '%s' "$val" | python3 "$KEYCHAIN_PY" set "$key" -
    return $?
  fi
  case "$PLATFORM" in
    macos)
      # `security -i` reads the add command from stdin, so the secret never
      # lands on the `security` argv (visible to ps). printf is a bash
      # builtin, so no argv exposure there either. Double quotes/backslashes
      # are escaped for security's interactive tokenizer.
      local key_sec val_sec
      key_sec=${key//\\/\\\\}; key_sec=${key_sec//\"/\\\"}
      val_sec=${val//\\/\\\\}; val_sec=${val_sec//\"/\\\"}
      printf 'add-generic-password -U -s "%s" -a "%s" -w "%s"\n' \
        "$key_sec" "${USER:-claude}" "$val_sec" | security -i >/dev/null 2>&1
      ;;
    linux)
      require_cmd secret-tool "Install libsecret-tools (see doctor)."
      printf '%s' "$val" | secret-tool store --label="$key" service "$key" >/dev/null
      ;;
    windows)
      # Escape single quotes for PowerShell single-quoted strings ('' = literal ').
      local key_esc="${key//\'/\'\'}"
      # The secret is piped via stdin ([Console]::In) so it never appears on
      # the PowerShell argv (visible to process listings).
      printf '%s' "$val" | ps_run "
\$ErrorActionPreference='Stop'
\$raw = [Console]::In.ReadToEnd()
\$pwd = ConvertTo-SecureString \$raw -AsPlainText -Force
New-StoredCredential -Target '$key_esc' -UserName '${USER:-${USERNAME:-claude}}' -SecurePassword \$pwd -Persist LocalMachine | Out-Null
" >/dev/null
      ;;
    *)
      echo "ERR: unsupported platform" >&2; exit 2 ;;
  esac
}

do_delete() {
  local key="${1:-}"
  [ -z "$key" ] && { echo "usage: $0 delete <key>" >&2; exit 3; }
  key=$(resolve_key "$key")
  if delegate_to_python; then
    python3 "$KEYCHAIN_PY" delete "$key" >/dev/null 2>&1 || true
    return 0
  fi
  case "$PLATFORM" in
    macos)   security delete-generic-password -s "$key" >/dev/null 2>&1 || true ;;
    linux)   secret-tool clear service "$key" >/dev/null 2>&1 || true ;;
    windows) local key_esc="${key//\'/\'\'}"; ps_run "Remove-StoredCredential -Target '$key_esc' -ErrorAction SilentlyContinue" >/dev/null 2>&1 || true ;;
  esac
}

do_list() {
  case "$PLATFORM" in
    macos)
      # Read `svce` - the SERVICE attribute, which is what `set` writes with -s and
      # `get` looks up with -s. This used to read `0x00000007`, which is the LABEL:
      # it happens to match for items written by keychain.py (which stamps both -l and
      # -s), but it is the wrong key in principle and diverges on any item whose label
      # was set independently, listing a name `get` cannot resolve.
      #
      # `dump-keychain` prints the first few attributes under numeric tags and the rest
      # under named ones, and `svce` is in the named group - so the tag to match is
      # `"svce"`, not `0x00000008`.
      #
      # Without `-d` this reads attributes only and does not prompt for access; only
      # the data-dumping form does.
      #
      # A trailing `\000` appears when security prints the value in its hex+C-string
      # form; it is not part of the service name.
      security dump-keychain 2>/dev/null \
        | awk -F'"' '/"svce"<blob>=/ && NF >= 5 { v = $4; sub(/\\000$/, "", v); if (v != "") print v }' \
        | sort -u
      ;;
    linux)
      secret-tool search --all 2>/dev/null \
        | awk -F'= ' '/^attribute\.service/{print $2}' \
        | sort -u
      ;;
    windows)
      ps_run "Get-StoredCredential | ForEach-Object { \$_.TargetName }" 2>/dev/null | tr -d '\r'
      ;;
  esac
}

do_doctor() {
  echo "Platform:        $PLATFORM"
  case "$PLATFORM" in
    macos)
      command -v security >/dev/null 2>&1 \
        && echo "Backend:         security ✓ (built-in)" \
        || echo "Backend:         security ✗ MISSING (impossible on macOS)"
      ;;
    linux)
      if command -v secret-tool >/dev/null 2>&1; then
        echo "Backend:         secret-tool ✓"
      else
        echo "Backend:         secret-tool ✗ MISSING"
        echo "Install:         sudo apt install libsecret-tools  # Debian/Ubuntu"
        echo "                 sudo dnf install libsecret        # Fedora/RHEL"
      fi
      ;;
    windows)
      if command -v pwsh >/dev/null 2>&1 || command -v powershell.exe >/dev/null 2>&1; then
        echo "Backend:         PowerShell ✓"
        local has_mod
        has_mod=$(ps_run "if (Get-Module -ListAvailable -Name CredentialManager) {'yes'} else {'no'}" 2>/dev/null | tr -d '\r' || echo "no")
        if [ "$has_mod" = "yes" ]; then
          echo "Module:          CredentialManager ✓"
        else
          echo "Module:          CredentialManager ✗ MISSING"
          echo "Install:         Install-Module CredentialManager -Scope CurrentUser -Force"
        fi
      else
        echo "Backend:         PowerShell ✗ MISSING"
      fi
      ;;
    *)
      # This used to advertise an env-var fallback. There isn't one here: `get` on an
      # unrecognised platform exits 2. Individual callers have their own env escape
      # hatches (figma-token.sh honours $FIGMA_PAT, for instance), but this helper
      # does not, and claiming otherwise sent people looking for a mechanism that
      # does not exist.
      echo "Backend:         none  -  unrecognised platform, \`get\` exits 2"
      echo "Workaround:      set the credential in the caller's own env var where one"
      echo "                 exists (e.g. FIGMA_PAT), or run on macOS / Linux / Windows"
      ;;
  esac
}

case "$CMD" in
  get)      do_get "$@" ;;
  set)      do_set "$@" ;;
  delete)   do_delete "$@" ;;
  list)     do_list ;;
  platform) echo "$PLATFORM" ;;
  doctor)   do_doctor ;;
  *) echo "usage: $0 {get|set|delete|list|platform|doctor} [args]" >&2; exit 3 ;;
esac
