#!/bin/sh
# observ-record.sh — fail-open observability recorder for git / Cursor hooks.
#
# Usage: observ-record.sh <hook-or-event-name> [args...]
#   Invoked by .git/hooks/* managed blocks and .cursor/hooks.json entries
#   materialized by `aaac update` (see project.yaml bindings.observability).
#
# Two responsibilities:
#
# 1. Generic recording — pipes the hook JSON payload (provided on stdin by
#    Cursor) into the stdin-aware `aaac-observ record-hook` command, which
#    resolves .agent-logs/config/event-mapping.json into 3-axis spans + links,
#    injects session_id, emits human-interaction events, and manages the
#    short-lived git context file (.agent-logs/.observ-context.json). When
#    event-mapping.json is absent it falls back to generic cursor.*/git.*
#    recording.
#
# 2. promotion-axis source of truth (#115) — on the git `post-commit` hook this
#    script records the canonical promotion.commit (close) event directly via
#    `aaac-observ record`, carrying commit.sha + committed_files (JSON array).
#    promotion.file events are NOT emitted by this hook — they are derived
#    in-process by Enricher R1/R2 from the committed_files array (#204).
#    These feed Enricher R1/R2/R5 (promotion parent/child + cross-axis links).
#
# session_id resolution (#115, multi-source priority):
#   (1) $AAAC_SESSION_ID env var   — propagated by @aaac/runtime when it spawns
#                                     git (observability-bridge.buildObservabilityEnv)
#   (2) .agent-logs/.observ-context.json (.session_id) guarded by a 5-minute TTL
#       on .created_at_ms — written by record-hook on `beforeShellExecution` for
#       git commit/push/merge (requires jq)
#   (3) "unknown"                   — fail-open default
#
# Recording must NEVER block the underlying operation: every external call is
# wrapped `|| true` and this script always exits 0 even when jq, git, or
# aaac-observ is missing or fails (AC-8).

EVENT_NAME="${1:-unknown}"

CONTEXT_FILE=".agent-logs/.observ-context.json"
CONTEXT_TTL_SECONDS=300   # 5-minute guard (#115)

# ── session_id resolution (multi-source priority) ──────────────────────────────
resolve_session_id() {
  # (1) env var set by @aaac/runtime when spawning git
  if [ -n "${AAAC_SESSION_ID:-}" ]; then
    printf '%s' "$AAAC_SESSION_ID"
    return 0
  fi

  # (2) short-lived git context file + TTL guard (jq required; skipped if absent)
  if [ -f "$CONTEXT_FILE" ] && command -v jq >/dev/null 2>&1; then
    _sid=$(jq -r '.session_id // empty' "$CONTEXT_FILE" 2>/dev/null)
    _created_ms=$(jq -r '.created_at_ms // empty' "$CONTEXT_FILE" 2>/dev/null)
    if [ -n "$_sid" ] && [ "$_sid" != "null" ] && [ -n "$_created_ms" ]; then
      _now_s=$(date +%s 2>/dev/null)
      # integer ms → s; non-numeric content evaluates to 0 → stale → ignored
      _created_s=$(( _created_ms / 1000 ))
      _age=$(( _now_s - _created_s ))
      if [ "$_age" -ge 0 ] && [ "$_age" -le "$CONTEXT_TTL_SECONDS" ]; then
        printf '%s' "$_sid"
        return 0
      fi
    fi
  fi

  # (3) fail-open
  printf 'unknown'
}

# ── JSON helpers (no jq dependency; fail-open best effort) ─────────────────────
# Escape a raw string for embedding inside a JSON double-quoted string literal.
json_escape() {
  printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}

# ── aaac-observ record wrapper (fail-open) ─────────────────────────────────────
# args: <event-type> <lifecycle> <session-id> <attr-json>
observ_record() {
  _trace_opt=""
  if [ -n "${AAAC_TRACE_ID:-}" ]; then
    _trace_opt="--trace-id ${AAAC_TRACE_ID}"
  fi
  # shellcheck disable=SC2086 # intentional word-splitting of optional flag
  npx -p @aaac/observability --no-install aaac-observ record \
    --event-type "$1" \
    --lifecycle "$2" \
    --source git-hook \
    --session-id "$3" \
    $_trace_opt \
    --attr "$4" \
    >/dev/null 2>&1 || true
}

# ── promotion-axis recording for post-commit (#115) ────────────────────────────
record_promotion_events() {
  _sid="$1"

  command -v git >/dev/null 2>&1 || return 0
  _sha=$(git rev-parse HEAD 2>/dev/null) || return 0
  [ -n "$_sha" ] || return 0

  # Repository root for repo-scoped worktree→commit attribution (#175). Best-effort.
  _repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || _repo_root=""

  # Newline-separated list of files in the just-created commit.
  # --root makes the initial (parentless) commit list its files too.
  _files=$(git diff-tree --no-commit-id --name-only -r --root HEAD 2>/dev/null)

  # Build committed_files as a JSON array string: ["a","b"].
  _committed_json='['
  _first=1
  _oldIFS=$IFS
  IFS='
'
  for _f in $_files; do
    [ -n "$_f" ] || continue
    _ef=$(json_escape "$_f")
    if [ "$_first" -eq 1 ]; then _first=0; else _committed_json="${_committed_json},"; fi
    _committed_json="${_committed_json}\"${_ef}\""
  done
  IFS=$_oldIFS
  _committed_json="${_committed_json}]"

  # promotion.commit (close). committed_files is a JSON-array *string* so the
  # Enricher (R1/R2/R5) can JSON.parse it. repo.root scopes the worktree→commit
  # attribution (#175 / R8). The --attr value is a JSON object to avoid the
  # comma-splitting of the key=value form.
  _commit_attr=$(printf '{"axis":"promotion","commit.sha":"%s","repo.root":"%s","committed_files":"%s"}' \
    "$(json_escape "$_sha")" "$(json_escape "$_repo_root")" "$(json_escape "$_committed_json")")
  observ_record "promotion.commit" "close" "$_sid" "$_commit_attr"

  # promotion.file is derived in-process by Enricher R1/R2 from committed_files.
  # No per-file npx invocation needed here (#204).
}

# ── main ───────────────────────────────────────────────────────────────────────
SESSION_ID=$(resolve_session_id)

case "$EVENT_NAME" in
  pre-commit|commit-msg|post-commit|pre-push|post-merge|post-checkout)
    # git hook: synthesize a minimal stdin payload so the generic git.* event
    # carries the resolved session_id (git hooks provide no Cursor JSON on stdin).
    printf '{"conversation_id":"%s"}' "$(json_escape "$SESSION_ID")" \
      | npx -p @aaac/observability --no-install aaac-observ record-hook "$EVENT_NAME" >/dev/null 2>&1 || true
    ;;
  *)
    # Cursor hook: pass the real stdin payload through untouched.
    npx -p @aaac/observability --no-install aaac-observ record-hook "$EVENT_NAME" >/dev/null 2>&1 || true
    ;;
esac

# promotion-axis source of truth: record commit/file events after the commit.
if [ "$EVENT_NAME" = "post-commit" ]; then
  record_promotion_events "$SESSION_ID"
fi

exit 0
