#!/usr/bin/env bash
# Native-CC admin PIN identity gate (Task 619).
#
# PreToolUse hook, registered under the universal (no-matcher) entry so it fires
# for EVERY tool call on a native-CC admin session. It denies all tools until the
# operator authenticates: the agent must call `admin-identity-authenticate` with a
# PIN that the loopback validate endpoint matches against users.json. On match the
# hook records per-session auth state (keyed by the conversation `session_id` it
# reads from stdin) and lets every subsequent tool through. Enforcement is here —
# a model that skips the prompt cannot reach any other tool.
#
# Scope: only native loopback admin sessions (MAXY_SESSION_ROLE=admin). Specialist
# subagents dispatched via the Task tool share the parent conversation's
# session_id, so they pass once the operator session is authenticated.
#
# Protocol: exit 0 = allow, exit 2 = block (stderr message shown to the agent),
# matching the sibling gate hooks. Fail-closed on terminal stdin.
#
# Lifecycle (one [admin-identity] line per step, correlated by sessionId):
#   op=loopback-spawn-received → op=pin-prompted → (op=pin-bound source=pin |
#   op=pin-miss → op=session-terminated) ; the MCP tool emits op=about-owner-loaded.

set -uo pipefail

if [ -t 0 ]; then
  echo "Blocked: pin-identity-gate received no stdin (cannot inspect tool call). Failing closed." >&2
  exit 2
fi
INPUT=$(cat)

# Only native loopback admin OPERATOR sessions are in scope:
#  - MAXY_SESSION_ROLE=admin excludes public/access sessions.
#  - MAXY_SPECIALIST empty excludes pty-spawned specialist/recorder sessions,
#    which carry role=admin but are not human operators and never enter a PIN
#    (mirrors the recursion guard the other admin hooks use). Task-tool
#    specialist subagents share the operator's session_id + empty MAXY_SPECIALIST,
#    so they pass via the operator's auth-state file once it is bound.
[ "${MAXY_SESSION_ROLE:-}" = "admin" ] || exit 0
[ -z "${MAXY_SPECIALIST:-}" ] || exit 0

# ----- Parse fields from stdin (python3, robust on nested tool_input) -------
SESSION_ID=$(printf '%s' "$INPUT" | python3 -c 'import json,sys
try: d=json.load(sys.stdin)
except Exception: d={}
print(d.get("session_id","") or "")' 2>/dev/null)
TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c 'import json,sys
try: d=json.load(sys.stdin)
except Exception: d={}
print(d.get("tool_name","") or "")' 2>/dev/null)

SID8="${SESSION_ID:0:8}"; [ -z "$SID8" ] && SID8='-'
log() { echo "[admin-identity] $1" >&2; }

# ----- Resolve account/state dir -------------------------------------------
# The account dir basename IS the ACCOUNT_ID (resolve-account-dir.sh), and the
# rc child env carries ACCOUNT_ID, so resolve deterministically rather than
# `find | head -1` — which does not skip dotfiles and could pick the sibling
# `data/accounts/.trash/`, diverging from the manager's `spawnCwd` (= the
# account dir) and stranding state the standing audit can never reconcile.
HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
PLATFORM_ROOT_RESOLVED="${HOOK_DIR}/../../.."
ACCOUNTS_DIR="${PLATFORM_ROOT_RESOLVED}/../data/accounts"
ACCOUNT_DIR=""
if [ -n "${ACCOUNT_ID:-}" ]; then
  ACCOUNT_DIR="${ACCOUNTS_DIR}/${ACCOUNT_ID}"
elif [ -d "$ACCOUNTS_DIR" ]; then
  # Defensive fallback only when ACCOUNT_ID is unset — exclude dot-entries.
  ACCOUNT_DIR=$(find "$ACCOUNTS_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null | head -1)
fi
STATE_DIR="${MAXY_IDENTITY_GATE_STATE_DIR:-${ACCOUNT_DIR}/state/admin-identity}"
STATE_FILE="${STATE_DIR}/${SESSION_ID}.json"

AUTH_TOOL='mcp__plugin_admin_admin__admin-identity-authenticate'
VALIDATE_URL="${MAXY_IDENTITY_VALIDATE_URL:-http://127.0.0.1:${MAXY_UI_INTERNAL_PORT:-0}/api/admin/identity/validate}"

# Already authenticated for this conversation → pass everything through.
if [ -n "$SESSION_ID" ] && [ -f "$STATE_FILE" ]; then
  exit 0
fi

log "op=loopback-spawn-received sessionId=${SID8}"

# ----- The authenticate tool: validate, bind on match, terminate on miss ----
if [ "$TOOL_NAME" = "$AUTH_TOOL" ]; then
  PIN=$(printf '%s' "$INPUT" | python3 -c 'import json,sys
try: d=json.load(sys.stdin)
except Exception: d={}
ti=d.get("tool_input") or {}
print(ti.get("pin","") or "")' 2>/dev/null)

  REQ=$(python3 -c 'import json,sys
print(json.dumps({"pin": sys.argv[1], "sessionId": sys.argv[2]}))' "$PIN" "$SESSION_ID" 2>/dev/null)
  RESP=$(printf '%s' "$REQ" | curl -s --max-time 5 -X POST -H 'content-type: application/json' -d @- "$VALIDATE_URL" 2>/dev/null)
  KIND=$(printf '%s' "$RESP" | python3 -c 'import json,sys
try: print((json.load(sys.stdin) or {}).get("kind","") or "")
except Exception: print("")' 2>/dev/null)

  case "$KIND" in
    match)
      USERID=$(printf '%s' "$RESP" | python3 -c 'import json,sys
print(json.load(sys.stdin).get("userId","") or "")' 2>/dev/null)
      mkdir -p "$STATE_DIR" 2>/dev/null
      printf '{"userId":%s,"ts":"%s"}' \
        "$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$USERID")" \
        "$(date -u +%FT%TZ)" > "$STATE_FILE" 2>/dev/null
      log "op=pin-bound sessionId=${SID8} userId=${USERID:0:8} source=pin authSurface=loopback"
      exit 0
      ;;
    miss)
      log "op=pin-miss sessionId=${SID8} reason=no-matching-user"
      log "op=session-terminated sessionId=${SID8} reason=pin-auth-failed"
      echo "PIN did not match. This session is being terminated. Start a new session and enter a valid PIN." >&2
      exit 2
      ;;
    unavailable)
      log "op=pin-unavailable sessionId=${SID8} reason=users-json-empty"
      echo "No admin PIN is configured for this install. Complete onboarding before using the admin agent." >&2
      exit 2
      ;;
    corrupt)
      log "op=pin-error sessionId=${SID8} reason=users-json-corrupt"
      echo "Admin user configuration is corrupt. Re-run the seed script before authenticating." >&2
      exit 2
      ;;
    *)
      log "op=pin-error sessionId=${SID8} reason=validate-unreachable"
      echo "Identity validation is unavailable; cannot proceed without authentication." >&2
      exit 2
      ;;
  esac
fi

# ----- Unauthenticated and not the auth tool → block -----------------------
log "op=pin-prompted sessionId=${SID8}"
echo "Authenticate first: call admin-identity-authenticate with the operator's PIN. No other tool is reachable until a matching PIN is bound for this session." >&2
exit 2
