#!/usr/bin/env bash
# gstack-config — read/write ~/.gstack/config.yaml
#
# Usage:
#   gstack-config get <key>          — read a config value (falls back to DEFAULTS)
#   gstack-config set <key> <value>  — write a config value
#   gstack-config list               — show all config (values + defaults)
#   gstack-config defaults           — show just the defaults table
#
# Env overrides (for testing):
#   GSTACK_STATE_ROOT — override ~/.gstack state directory (highest priority,
#                       matches D16 cathedral isolation convention)
#   GSTACK_HOME       — override ~/.gstack state directory (aligns with writer scripts)
#   GSTACK_STATE_DIR  — legacy alias for GSTACK_HOME (kept for backwards compat)
set -euo pipefail

STATE_DIR="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}}"
CONFIG_FILE="$STATE_DIR/config.yaml"

# Annotated header for new config files. Written once on first `set`.
# Default semantics: DEFAULTS table below is the canonical source. Header text
# is documentation that must stay in sync with DEFAULTS.
CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on next skill run.
# Docs: https://github.com/garrytan/gstack
#
# ─── Behavior ────────────────────────────────────────────────────────
# proactive: true           # Auto-invoke skills when your request matches one.
#                           # Set to false to only run skills you type explicitly.
#
# routing_declined: false   # Set to true to skip the CLAUDE.md routing injection
#                           # prompt. Set back to false to be asked again.
#
# ─── Telemetry ───────────────────────────────────────────────────────
# telemetry: off            # off | anonymous | community
#                           #   off       — no data sent, no local analytics (default)
#                           #   anonymous — counter only, no device ID
#                           #   community — usage data + stable device ID
#
# ─── Updates ─────────────────────────────────────────────────────────
# auto_upgrade: false       # true = silently upgrade on session start
# update_check: true        # false = suppress version check notifications
#
# ─── Skill naming ────────────────────────────────────────────────────
# skill_prefix: false       # true = namespace skills as /gstack-qa, /gstack-ship
#                           # false = short names /qa, /ship
#
# ─── Checkpoint ──────────────────────────────────────────────────────
# checkpoint_mode: explicit # explicit | continuous
#                           #   explicit   — commit only when you run /ship or /checkpoint
#                           #   continuous — auto-commit after each significant change
#                           #                with WIP: prefix + [gstack-context] body
#
# checkpoint_push: false    # true = push WIP commits to remote as you go
#                           # false = keep WIP commits local only (default)
#                           # Pushing can trigger CI/deploy hooks — opt in carefully.
#
# ─── Writing style (V1) ──────────────────────────────────────────────
# explain_level: default    # default = jargon-glossed, outcome-framed prose
#                           #           (V1 default — more accessible for everyone)
#                           # terse   = V0 prose style, no glosses, no outcome-framing layer
#                           #           (for power users who know the terms)
#                           # Unknown values default to "default" with a warning.
#                           # See docs/designs/PLAN_TUNING_V1.md for rationale.
#
# ─── Artifacts sync (renamed from gbrain_sync_mode in v1.27.0.0) ─────
# artifacts_sync_mode: off  # off | artifacts-only | full
#                           #   off            — no sync (default)
#                           #   artifacts-only — sync plans/designs/retros/learnings only
#                           #                    (skip behavioral data: question-log,
#                           #                    developer-profile, timeline)
#                           #   full           — sync everything allowlisted
#                           # Set by the first-run privacy stop-gate. See docs/gbrain-sync.md.
#
# artifacts_sync_mode_prompted: false
#                           # Set to true once the privacy gate has asked the user.
#                           # Flip back to false to be re-prompted.
#
# ─── Plan-tune hooks ─────────────────────────────────────────────────
# plan_tune_hooks: prompt   # Controls whether ./setup installs the plan-tune
#                           #   Claude Code hooks (PostToolUse capture +
#                           #   PreToolUse preference enforcement).
#                           #   prompt — ask on a real TTY, skip otherwise (default)
#                           #   yes    — install non-interactively
#                           #   no     — skip non-interactively
#                           # Override per-run: ./setup --plan-tune-hooks /
#                           #   --no-plan-tune-hooks, or env GSTACK_PLAN_TUNE_HOOKS.
#
# ─── Advanced ────────────────────────────────────────────────────────
# codex_reviews: enabled    # disabled = skip Codex adversarial reviews in /ship
# gstack_contributor: false # true = file field reports when gstack misbehaves
# skip_eng_review: false    # true = skip eng review gate in /ship (not recommended)
#
# ─── Workspace-aware ship ────────────────────────────────────────────
# workspace_root: $HOME/conductor/workspaces  # Where /ship looks for sibling
#                           # Conductor worktrees when picking a VERSION slot.
#                           # Set to "null" to disable sibling scanning entirely.
#                           # Non-Conductor users can point this at any directory
#                           # that holds parallel worktrees of the same repo.
#
'

# DEFAULTS table — canonical default values for known keys.
# `get <key>` returns DEFAULTS[key] when the key is absent from the config file
# AND the env override is not set. Keep in sync with the CONFIG_HEADER comments.
lookup_default() {
  case "$1" in
    proactive) echo "true" ;;
    routing_declined) echo "false" ;;
    telemetry) echo "off" ;;
    auto_upgrade) echo "false" ;;
    update_check) echo "true" ;;
    skill_prefix) echo "false" ;;
    checkpoint_mode) echo "explicit" ;;
    checkpoint_push) echo "false" ;;
    explain_level) echo "default" ;;
    codex_reviews) echo "enabled" ;;
    gstack_contributor) echo "false" ;;
    skip_eng_review) echo "false" ;;
    workspace_root) echo "$HOME/conductor/workspaces" ;;
    cross_project_learnings) echo "" ;; # intentionally empty → unset triggers first-time prompt
    artifacts_sync_mode) echo "off" ;;
    artifacts_sync_mode_prompted) echo "false" ;;
    plan_tune_hooks) echo "prompt" ;; # prompt | yes | no — controls ./setup plan-tune hook install

    redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection
    redact_prepush_hook) echo "false" ;;
    # Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline:
    #   brain_trust_policy@<hash>  — unset on fresh install; setup-gbrain
    #                                writes 'personal' for local engines,
    #                                asks the user for remote-ambiguous.
    #   salience_allowlist          — empty falls through to
    #                                SALIENCE_DEFAULT_ALLOWLIST (D9).
    #   user_slug_at_<hash>         — empty triggers resolve-user-slug
    #                                fallback chain (D4 A3) on first call.
    brain_trust_policy*) echo "unset" ;;
    salience_allowlist) echo "" ;;
    user_slug_at_*) echo "" ;;
    *) echo "" ;;
  esac
}

# ──────────────────────────────────────────────────────────────────────
# Brain-integration helpers (T5+T10+T16)
# ──────────────────────────────────────────────────────────────────────

# Compute sha8 of a string. Used for endpoint hashing.
sha8_of() {
  printf '%s' "$1" | shasum -a 256 | cut -c1-8
}

# Detect the active brain endpoint hash. Reads ~/.claude.json for the gbrain
# MCP server URL. Falls back to the literal 'local' when no MCP is configured.
endpoint_hash() {
  _claude_json="$HOME/.claude.json"
  if [ -f "$_claude_json" ] && command -v jq >/dev/null 2>&1; then
    _url=$(jq -r '.mcpServers.gbrain.url // .mcpServers.gbrain.transport.url // empty' "$_claude_json" 2>/dev/null)
    if [ -n "$_url" ] && [ "$_url" != "null" ]; then
      sha8_of "$_url"
      return 0
    fi
  fi
  printf '%s' "local"
}

# Detect endpoint hash collisions. When two distinct endpoints share the same
# sha8 prefix (rare but possible), escalate to sha16 by emitting the longer
# hash. Detection: scan config file for existing brain_trust_policy@<hash> or
# user_slug_at_<hash> keys; if any non-active hash equals the active sha8 but
# would differ at sha16, the active endpoint needs sha16.
endpoint_hash_with_collision_check() {
  _active=$(endpoint_hash)
  if [ "$_active" = "local" ]; then
    printf '%s' "$_active"
    return 0
  fi
  # If a different endpoint (different URL) shares this sha8, escalate.
  # We only catch this when the config has another endpoint recorded.
  _matching=$(grep -E "^(brain_trust_policy|user_slug_at)@${_active}" "$CONFIG_FILE" 2>/dev/null | head -1 || true)
  _claude_json="$HOME/.claude.json"
  if [ -n "$_matching" ] && [ -f "$_claude_json" ] && command -v jq >/dev/null 2>&1; then
    _url=$(jq -r '.mcpServers.gbrain.url // .mcpServers.gbrain.transport.url // empty' "$_claude_json" 2>/dev/null)
    _sha16=$(printf '%s' "$_url" | shasum -a 256 | cut -c1-16)
    # Look for any sha16-namespaced key that conflicts. If a stored sha16 exists
    # and differs from current sha16, that's the collision evidence; emit sha16.
    _stored16=$(grep -E "^(brain_trust_policy|user_slug_at)@${_sha16}" "$CONFIG_FILE" 2>/dev/null | head -1 || true)
    if [ -n "$_stored16" ]; then
      printf '%s' "$_sha16"
      return 0
    fi
  fi
  printf '%s' "$_active"
}

# Resolve the user-slug per D4 A3 chain:
#   1. mcp__gbrain__whoami.client_name (best effort via gbrain CLI shell-out)
#   2. $USER env
#   3. sha8($(git config user.email))
#   4. anonymous-<sha8(hostname)>
# Persists result via gstack-config set user_slug_at_<endpoint-hash> on first call.
resolve_user_slug() {
  _hash=$(endpoint_hash_with_collision_check)
  _stored=$(grep -E "^user_slug_at_${_hash}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
  if [ -n "$_stored" ]; then
    printf '%s' "$_stored"
    return 0
  fi

  _slug=""

  # Layer 1: gbrain whoami
  if command -v gbrain >/dev/null 2>&1; then
    _whoami=$(gbrain whoami --json 2>/dev/null || true)
    if [ -n "$_whoami" ] && command -v jq >/dev/null 2>&1; then
      _client_name=$(printf '%s' "$_whoami" | jq -r '.client_name // .token_name // empty' 2>/dev/null || true)
      if [ -n "$_client_name" ] && [ "$_client_name" != "null" ]; then
        _slug=$(printf '%s' "$_client_name" | tr '[:upper:] ' '[:lower:]-' | tr -dc '[:alnum:]-')
      fi
    fi
  fi

  # Layer 2: $USER
  if [ -z "$_slug" ] && [ -n "${USER:-}" ]; then
    _slug=$(printf '%s' "$USER" | tr '[:upper:] ' '[:lower:]-' | tr -dc '[:alnum:]-')
  fi

  # Layer 3: sha8 of git email
  if [ -z "$_slug" ]; then
    _email=$(git config user.email 2>/dev/null || true)
    if [ -n "$_email" ]; then
      _slug="email-$(sha8_of "$_email")"
    fi
  fi

  # Layer 4: anonymous-<sha8(hostname)>
  if [ -z "$_slug" ]; then
    _slug="anonymous-$(sha8_of "$(hostname 2>/dev/null || echo unknown)")"
  fi

  # Persist via direct file write (avoid recursion into gstack-config set)
  mkdir -p "$STATE_DIR"
  if [ ! -f "$CONFIG_FILE" ]; then
    printf '%s' "$CONFIG_HEADER" > "$CONFIG_FILE"
  fi
  if ! grep -qE "^user_slug_at_${_hash}:" "$CONFIG_FILE" 2>/dev/null; then
    echo "user_slug_at_${_hash}: ${_slug}" >> "$CONFIG_FILE"
  fi

  printf '%s' "$_slug"
}

case "${1:-}" in
  get)
    KEY="${2:?Usage: gstack-config get <key>}"
    # Validate key (alphanumeric + underscore + optional @<hash> suffix for
    # endpoint-namespaced keys introduced by the brain-aware planning layer)
    if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then
      echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix" >&2
      exit 1
    fi
    # Use literal match for keys containing @ (sha hashes), regex otherwise
    VALUE=$(grep -F "${KEY}:" "$CONFIG_FILE" 2>/dev/null | grep -E "^${KEY%@*}(@[a-f0-9]+)?:" | grep -F "${KEY}:" | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
    if [ -z "$VALUE" ]; then
      VALUE=$(lookup_default "$KEY")
    fi
    printf '%s' "$VALUE"
    ;;
  set)
    KEY="${2:?Usage: gstack-config set <key> <value>}"
    VALUE="${3:?Usage: gstack-config set <key> <value>}"
    # Validate key (alphanumeric + underscore + optional @<hash> suffix)
    if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then
      echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix" >&2
      exit 1
    fi
    # Validate brain_trust_policy value domain (D4 / D11)
    if printf '%s' "$KEY" | grep -qE '^brain_trust_policy(@|$)' && \
       [ "$VALUE" != "personal" ] && [ "$VALUE" != "shared" ] && [ "$VALUE" != "unset" ]; then
      echo "Warning: brain_trust_policy '$VALUE' not recognized. Valid values: personal, shared, unset. Using unset." >&2
      VALUE="unset"
    fi
    # V1: whitelist values for keys with closed value domains. Unknown values warn + default.
    if [ "$KEY" = "explain_level" ] && [ "$VALUE" != "default" ] && [ "$VALUE" != "terse" ]; then
      echo "Warning: explain_level '$VALUE' not recognized. Valid values: default, terse. Using default." >&2
      VALUE="default"
    fi
    if [ "$KEY" = "artifacts_sync_mode" ] && [ "$VALUE" != "off" ] && [ "$VALUE" != "artifacts-only" ] && [ "$VALUE" != "full" ]; then
      echo "Warning: artifacts_sync_mode '$VALUE' not recognized. Valid values: off, artifacts-only, full. Using off." >&2
      VALUE="off"
    fi
    # redact_repo_visibility: a LOCAL override for repos gh/glab can't read (e.g.
    # self-hosted GitLab). It lives in ~/.gstack/config.yaml (never committed), so
    # it can't be used to weaken the gate repo-wide for other contributors.
    if [ "$KEY" = "redact_repo_visibility" ] && [ "$VALUE" != "public" ] && [ "$VALUE" != "private" ] && [ "$VALUE" != "unknown" ]; then
      echo "Warning: redact_repo_visibility '$VALUE' not recognized. Valid values: public, private, unknown. Using unknown." >&2
      VALUE="unknown"
    fi
    if [ "$KEY" = "redact_prepush_hook" ] && [ "$VALUE" != "true" ] && [ "$VALUE" != "false" ]; then
      echo "Warning: redact_prepush_hook '$VALUE' not recognized. Valid values: true, false. Using false." >&2
      VALUE="false"
    fi
    if [ "$KEY" = "plan_tune_hooks" ] && [ "$VALUE" != "prompt" ] && [ "$VALUE" != "yes" ] && [ "$VALUE" != "no" ]; then
      echo "Warning: plan_tune_hooks '$VALUE' not recognized. Valid values: prompt, yes, no. Using prompt." >&2
      VALUE="prompt"
    fi
    mkdir -p "$STATE_DIR"
    # Write annotated header on first creation
    if [ ! -f "$CONFIG_FILE" ]; then
      printf '%s' "$CONFIG_HEADER" > "$CONFIG_FILE"
    fi
    # Escape sed special chars in value and drop embedded newlines
    ESC_VALUE="$(printf '%s' "$VALUE" | head -1 | sed 's/[&/\]/\\&/g')"
    if grep -qE "^${KEY}:" "$CONFIG_FILE" 2>/dev/null; then
      # Portable in-place edit (BSD sed uses -i '', GNU sed uses -i without arg)
      _tmpfile="$(mktemp "${CONFIG_FILE}.XXXXXX")"
      sed "/^${KEY}:/s/.*/${KEY}: ${ESC_VALUE}/" "$CONFIG_FILE" > "$_tmpfile" && mv "$_tmpfile" "$CONFIG_FILE"
    else
      echo "${KEY}: ${VALUE}" >> "$CONFIG_FILE"
    fi
    # Auto-relink skills when prefix setting changes (skip during setup to avoid recursive call)
    if [ "$KEY" = "skill_prefix" ] && [ -z "${GSTACK_SETUP_RUNNING:-}" ]; then
      GSTACK_RELINK="$(dirname "$0")/gstack-relink"
      [ -x "$GSTACK_RELINK" ] && "$GSTACK_RELINK" || true
    fi
    ;;
  list)
    if [ -f "$CONFIG_FILE" ]; then
      cat "$CONFIG_FILE"
    fi
    echo ""
    echo "# ─── Active values (including defaults for unset keys) ───"
    for KEY in proactive routing_declined telemetry auto_upgrade update_check \
               skill_prefix checkpoint_mode checkpoint_push explain_level \
               codex_reviews gstack_contributor skip_eng_review workspace_root \
               artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do
      VALUE=$(grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
      SOURCE="default"
      if [ -n "$VALUE" ]; then
        SOURCE="set"
      else
        VALUE=$(lookup_default "$KEY")
      fi
      printf '  %-24s %s (%s)\n' "$KEY:" "$VALUE" "$SOURCE"
    done
    ;;
  defaults)
    echo "# gstack-config defaults"
    for KEY in proactive routing_declined telemetry auto_upgrade update_check \
               skill_prefix checkpoint_mode checkpoint_push explain_level \
               codex_reviews gstack_contributor skip_eng_review workspace_root \
               artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do
      printf '  %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
    done
    ;;
  endpoint-hash)
    # Brain integration helper (T10): print active brain endpoint sha8
    endpoint_hash_with_collision_check
    ;;
  resolve-user-slug)
    # Brain integration helper (T16 / D4 A3): resolve + persist user-slug
    resolve_user_slug
    ;;
  gbrain-refresh)
    # Brain integration helper: re-detect gbrain installation state and
    # persist to ~/.gstack/gbrain-detection.json. gen-skill-docs reads this
    # file (when invoked with --respect-detection) to decide whether to
    # render GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS blocks in
    # generated SKILL.md files.
    #
    # Run this after installing or uninstalling gbrain so your locally
    # generated SKILL.md files match your installation state.
    SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
    DETECT_BIN="$SCRIPT_DIR/gstack-gbrain-detect"
    DETECTION_FILE="$STATE_DIR/gbrain-detection.json"
    mkdir -p "$STATE_DIR"
    if [ ! -x "$DETECT_BIN" ]; then
      echo "gstack-gbrain-detect not found at $DETECT_BIN" >&2
      exit 1
    fi
    if ! "$DETECT_BIN" > "$DETECTION_FILE.tmp" 2>/dev/null; then
      printf '{"gbrain_on_path":false,"gbrain_local_status":"no-cli"}\n' > "$DETECTION_FILE.tmp"
    fi
    mv "$DETECTION_FILE.tmp" "$DETECTION_FILE"

    # Summarize for the user. Use python (already required elsewhere) to
    # parse the JSON portably; fall back to grep if python is unavailable.
    PYTHON_CMD=$(command -v python3 || command -v python || true)
    if [ -n "$PYTHON_CMD" ]; then
      STATUS=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_local_status','unknown'))" 2>/dev/null || echo unknown)
      VERSION=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_version') or 'unknown')" 2>/dev/null || echo unknown)
    else
      STATUS=$(grep -o '"gbrain_local_status":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/\1/')
      VERSION=$(grep -o '"gbrain_version":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/\1/')
      [ -z "$STATUS" ] && STATUS=unknown
      [ -z "$VERSION" ] && VERSION=unknown
    fi

    case "$STATUS" in
      ok)
        echo "Detected gbrain v$VERSION → brain-aware blocks will render in planning-skill SKILL.md files."
        echo "Run 'bun run gen:skill-docs' in the gstack repo (or re-run ./setup) to regenerate now."
        ;;
      *)
        echo "gbrain not detected (local-status: $STATUS) → brain-aware blocks will be suppressed in planning-skill SKILL.md files."
        echo "Install gbrain (see /setup-gbrain) and re-run 'gstack-config gbrain-refresh' once it's configured."
        ;;
    esac
    ;;
  *)
    echo "Usage: gstack-config {get|set|list|defaults|endpoint-hash|resolve-user-slug|gbrain-refresh} [key] [value]"
    exit 1
    ;;
esac
