#!/usr/bin/env bash
# Archive-ingest surface gate (updated).
#
# Five enforcements, one script — phase decided by `hook_event_name` on stdin.
# narrows the database-operator subagent's effective surface during
# WhatsApp archive ingestion to exactly one Bash entry
# (`memory/bin/conversation-archive-ingest.sh`) plus read-only neighbours, by
# blocking the legacy MCP deviation tools mechanically. The retired
# `whatsapp-export-insight-pass` tool is replaced by Phase 2:
# `mcp__memory__conversation-archive-derive-insights` — a read-only tool that
# walks :Section chunks of one named :ConversationArchive in
# pages and emits per-row proposals. The new tool is NOT in any BLOCK list
# (the gate is allow-by-default for unrecognised tools) — its writes go
# through the existing graph-cypher-write surface, gated by the operator per
# row in the conversation-archive-enrich skill. Stale references to the
# retired Phase 2 name (`whatsapp-export-insight-pass`) remain in the BLOCK
# list as a loud-denial breadcrumb for any operator-edited skill that still
# names them.
#
# 1. PreToolUse on the four legacy WhatsApp MCP tools — BLOCK unconditionally.
#    The single deterministic Bash entry is the only supported path for
#    `archiveType=whatsapp-export`. Tool source for #1-#3 remains until cleanup;
#    `whatsapp-export-insight-pass` source was deleted and the
#    block here catches stale references.
#       mcp__memory__whatsapp-export-parse
#       mcp__memory__whatsapp-export-insight-write
#       mcp__memory__whatsapp-export-insight-pass     (deleted, retired)
#       mcp__memory__memory-archive-write             (only when `archiveType` is
#                                                      `whatsapp-export`; LinkedIn
#                                                      and other archiveTypes pass
#                                                      through unchanged.)
#
# 2. PreToolUse Edit/Write/NotebookEdit: deny writes under
#    `*platform/plugins/*/lib/*` (parser/CSV-shape source for any *-import or
#    *-export plugin). Preserved — defence in depth even though
#    the database-operator template no longer lists Edit/Write tools.
#
# 3. PreToolUse Bash: deny commands invoking JavaScript test runners
#    (vitest|bun test|npm test|npx jest|node.*vitest). Preserved.
#
# 4. Parse-error gate: PostToolUse on any `mcp__*__*-export-parse` /
#    `mcp__*__*-import-parse` tool whose `tool_response.isError == true`
#    writes a flag file. Subsequent PreToolUse on ANY tool blocks until
#    UserPromptSubmit clears the flag. A 600s TTL is the cross-session safety
#    net. Preserved because LinkedIn and future per-source archive
#    parsers still use the legacy MCP path until they migrate to their own
#    deterministic Bash entries.
#
# 5. Logging: every PreToolUse decision emits one line in the format
#       [archive-ingest-gate] decision=<allow|block> tool=<name> reason=<r> ...
#    so the operator can grep the full decision trail for one ingest from
#    server.log alongside the [whatsapp-ingest] script lines.
#
# Exit codes follow Claude Code hook protocol: 0 = allow, 2 = block (stderr
# message shown to the agent). Fail-closed on terminal stdin.

set -uo pipefail

# Read stdin — fail closed if attached to a terminal (no JSON envelope coming).
if [ -t 0 ]; then
  echo "Blocked: archive-ingest-surface-gate received no stdin (cannot inspect tool call). Failing closed." >&2
  exit 2
fi
INPUT=$(cat)

# ----- Resolve account dir for state file ----------------------------------
HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
PLATFORM_ROOT_RESOLVED="${HOOK_DIR}/../../.."
ACCOUNTS_DIR="${PLATFORM_ROOT_RESOLVED}/../data/accounts"
ACCOUNT_DIR=""
if [ -d "$ACCOUNTS_DIR" ]; then
  ACCOUNT_DIR=$(find "$ACCOUNTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -1)
fi
STATE_DIR="${ARCHIVE_INGEST_GATE_STATE_DIR:-${ACCOUNT_DIR}/state}"
FLAG_FILE="${STATE_DIR}/archive-ingest-parse-error.flag"
TTL_SECONDS=600

# ----- Parse fields from stdin ---------------------------------------------
HOOK_EVENT=$(printf '%s' "$INPUT" | grep -o '"hook_event_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
TOOL_NAME=$(printf '%s' "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')

# ============================================================================
# UserPromptSubmit — clear the parse-error flag.
# ============================================================================
if [ "$HOOK_EVENT" = "UserPromptSubmit" ]; then
  if [ -f "$FLAG_FILE" ]; then
    rm -f "$FLAG_FILE" 2>/dev/null
    echo "[archive-ingest-gate] cleared reason=user-prompt-submit" >&2
  fi
  exit 0
fi

# ============================================================================
# PostToolUse — record parse-error from any *-export-parse / *-import-parse.
# ============================================================================
if [ "$HOOK_EVENT" = "PostToolUse" ]; then
  case "$TOOL_NAME" in
    mcp__*__*-export-parse|mcp__*__*-import-parse)
      if printf '%s' "$INPUT" | grep -Eq '"isError"[[:space:]]*:[[:space:]]*true'; then
        mkdir -p "$STATE_DIR" 2>/dev/null
        date -u +%s > "$FLAG_FILE" 2>/dev/null
        echo "[archive-ingest-gate] surfaced reason=parse-error tool=${TOOL_NAME}" >&2
      fi
      ;;
  esac
  exit 0
fi

# ============================================================================
# PreToolUse — five independent blocks.
# ============================================================================
if [ "$HOOK_EVENT" != "PreToolUse" ]; then
  exit 0
fi

# Helper: emit a single decision-log line + remediation message, then exit.
# Args: $1=decision (allow|block) $2=reason $3=remediation message
emit_decision() {
  local decision="$1" reason="$2" remediation="$3" cmd="${4:-}"
  local cmd_field=""
  if [ -n "$cmd" ]; then
    # Sanitise: strip control chars, take first 60 chars.
    cmd_field=" command=\"$(printf '%s' "$cmd" | tr -d '\000-\037' | cut -c1-60)\""
  fi
  echo "[archive-ingest-gate] decision=${decision} tool=${TOOL_NAME} reason=${reason}${cmd_field}" >&2
  if [ "$decision" = "block" ]; then
    echo "$remediation" >&2
    exit 2
  fi
}

# --- Block 1: post-parse-error gate (applies to ALL tools) -----------------
if [ -f "$FLAG_FILE" ]; then
  FLAG_TS=$(cat "$FLAG_FILE" 2>/dev/null | head -1)
  NOW=$(date -u +%s)
  if [ -n "$FLAG_TS" ] && [ "$FLAG_TS" -gt 0 ] 2>/dev/null; then
    AGE=$(( NOW - FLAG_TS ))
    if [ "$AGE" -lt "$TTL_SECONDS" ]; then
      emit_decision "block" "post-parse-error age_s=${AGE}" \
        "Blocked: an archive-parser MCP tool returned isError=true earlier in this turn. The subagent's next action must be a user-facing message naming the parse-error and yielding back to the operator. Further tool calls are blocked until the operator submits a new prompt."
    fi
    rm -f "$FLAG_FILE" 2>/dev/null
  else
    rm -f "$FLAG_FILE" 2>/dev/null
  fi
fi

# --- Block 2: legacy WhatsApp MCP tools — defensive denial ----------------
# These tool sources were deleted; the block stays as a stale-
# reference catch (skill markdown checked into older installs may still name
# them). The harness will return tool-not-found anyway, but a named block
# message guides the operator to the new entry.
case "$TOOL_NAME" in
  mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write|mcp__memory__whatsapp-export-insight-pass|mcp__memory__whatsapp-export-preview)
    emit_decision "block" "denied-mcp-legacy" \
      "Blocked: ${TOOL_NAME} is a retired path. Invoke the generalised conversation-archive skill — 'bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source whatsapp --participant-person-ids <csv> --scope <admin|public>' (owner derived from ACCOUNT_ID + USER_ID env). The script normalises, sessionizes, and emits prepared sessions; the dispatched specialist classifies each session in-turn and calls memory-ingest with conversationIdentity set (writes :ConversationArchive)."
    ;;
esac

# Helper: extract a top-level field from `tool_input` via python3 — never via
# grep+sed against the raw JSON, which would pick the first textual occurrence
# including nested-object matches (`rows[0].archiveType`,
# `conversation.archiveType`, etc.) and let a malicious payload bypass the
# block. python3 is the project standard for JSON-aware hook parsing
# (mirrors lane-gate.sh:31-46). On parse failure return empty string —
# downstream block conditions skip cleanly.
extract_tool_input_field() {
  local field="$1"
  printf '%s' "$INPUT" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    print(d.get('tool_input', {}).get('$field', ''))
except Exception:
    print('')
" 2>/dev/null || echo ""
}

# --- Block 3: memory-archive-write conditional on conversation-shaped types
# LinkedIn-connections and future flat-dataset archiveTypes flow unchanged.
# `whatsapp-export` was the only conversation-shaped archiveType
# deleted that handler entirely, so the type itself is now invalid input —
# the block stays defensive against operator-edited skills that still name it.
if [ "$TOOL_NAME" = "mcp__memory__memory-archive-write" ]; then
  ARCHIVE_TYPE=$(extract_tool_input_field archiveType)
  if [ "$ARCHIVE_TYPE" = "whatsapp-export" ]; then
    emit_decision "block" "denied-mcp-legacy archiveType=whatsapp-export" \
      "Blocked: memory-archive-write with archiveType='whatsapp-export' is a retired path. Routes conversation transcripts through 'bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source whatsapp --participant-person-ids <csv> --scope <admin|public>' (owner derived from ACCOUNT_ID + USER_ID env). Flat-dataset archiveTypes (linkedin-connections, …) flow through memory-archive-write unchanged."
  fi
fi

# --- Block 4: plugin-source path block (Edit/Write/NotebookEdit) -----------
case "$TOOL_NAME" in
  Edit|Write|NotebookEdit)
    FILE_PATH=$(extract_tool_input_field file_path)
    case "$FILE_PATH" in
      */platform/plugins/*/lib/*|platform/plugins/*/lib/*)
        emit_decision "block" "plugin-source-edit path=${FILE_PATH}" \
          "Blocked: ${TOOL_NAME} on ${FILE_PATH} is a platform plugin lib/ path. The database-operator subagent does not own plugin source; if a parser is broken, surface the parse-error to the operator and let them dispatch a code-edit task instead."
        ;;
    esac
    ;;
esac

# --- Block 5: shell test-runner block (Bash) -------------------------------
COMMAND=""
if [ "$TOOL_NAME" = "Bash" ]; then
  COMMAND=$(extract_tool_input_field command)
  if printf '%s' "$COMMAND" | grep -Eq '(^|[[:space:]/])vitest($|[[:space:]])|(^|[[:space:]])bun[[:space:]]+test($|[[:space:]])|(^|[[:space:]])npm[[:space:]]+test($|[[:space:]])|(^|[[:space:]])npx[[:space:]]+jest($|[[:space:]])|node[[:space:]].*vitest'; then
    emit_decision "block" "test-runner" \
      "Blocked: Bash command invokes a JavaScript test runner (vitest/bun test/npm test/npx jest). The database-operator subagent does not run plugin tests; surface the parse-error to the operator." \
      "$COMMAND"
  fi
fi

# Default — allow. Emit a single-line decision log so successful invocations
# leave a grep-able trail too. (Bash gets command field; other tools omit.)
if [ "$TOOL_NAME" = "Bash" ]; then
  emit_decision "allow" "default" "" "$COMMAND"
else
  emit_decision "allow" "default" ""
fi

exit 0
