#!/usr/bin/env bash
# fs-schema-guard-bash-post — PostToolUse Bash stray-folder detection.
#
# The other half of the Bash-channel net (see fs-schema-guard-bash-pre.sh). After
# a Bash command runs, this hook re-lists the account root, parses the account's
# allowed-top-level set, and flags entries that are present NOW, absent from the
# pre-snapshot, and not allowed — the strays THIS command created. For each it
# emits exit 2 with a schema-citing message so the agent self-corrects within the
# same turn.
#
# No filesystem mutation: PostToolUse runs after the command executed, so it
# cannot prevent the mkdir, and auto-deleting a just-created folder risks
# destroying legitimate in-flight work (auto-repair was rejected). The standing
# reconcile (account-dir-schema-reconcile.ts) is the net for anything the agent
# ignores or that predates the hook.
#
# Diffing against the pre-snapshot (not flagging every non-allowed entry) is what
# keeps the hook from firing on the legacy backlog on every unrelated Bash call.
#
# Exit codes: 0 = allow, 2 = feedback to the agent (stderr shown). Fail open
# (exit 0) when the snapshot is missing or the allowed set is empty. Every block
# logs: [fs-guard-bash] stray path=<name> reason=<top-level|bad-name>
# No task numbers / internal refs in any operator-visible string.

set -uo pipefail

# No stdin envelope (tty) — cannot inspect. Fail open (post hooks never block on
# an un-inspectable call; a false block on every terminal-run Bash is worse than
# a missed stray the reconcile still catches).
[ -t 0 ] && exit 0
INPUT=$(cat)
[ -z "$INPUT" ] && exit 0

field() {
  printf '%s' "$INPUT" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    print(d.get('$1', '') or '')
except Exception:
    print('')
" 2>/dev/null || echo ""
}

TOOL_NAME=$(field tool_name)
[ "$TOOL_NAME" = "Bash" ] || exit 0

SESSION_ID=$(field session_id)
[ -z "$SESSION_ID" ] && exit 0

if [ -n "${FS_GUARD_BASH_TMPDIR:-}" ]; then
  TMP_DIR="$FS_GUARD_BASH_TMPDIR"
else
  TMP_DIR="${TMPDIR:-${TMP:-${TEMP:-/tmp}}}"
  TMP_DIR="${TMP_DIR%/}"
fi
SID_SAN=$(printf '%s' "$SESSION_ID" | sed 's/[^A-Za-z0-9_-]/_/g')
SNAP="$TMP_DIR/maxy-fsguard-snapshot-${SID_SAN}.txt"

# Missing pre-snapshot -> fail open (the pre hook could not run or failed open).
[ -f "$SNAP" ] || exit 0

ACCOUNT_DIR="$PWD"

# Parse the allowed top-level set from the account's seeded SCHEMA.md — the same
# awk extraction fs-schema-guard.sh uses. Empty/absent -> fail open.
ALLOWED=""
if [ -f "$ACCOUNT_DIR/SCHEMA.md" ]; then
  ALLOWED=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
fi
[ -z "$ALLOWED" ] && exit 0

# Compute this command's findings, each tagged `reason<TAB>name`:
#   top-level : an account-root entry present now, absent from the pre-snapshot,
#               not in the allowed set (the original stray check).
#   bad-name  : an immediate child of an operator-data bucket (projects/contacts/
#               documents) that is NEW this command whose name breaks the
#               convention. A bucket absent from the pre-snapshot is new, so all
#               its children are this command's doing; flagging them cannot
#               re-spam the legacy backlog, which lives only in pre-existing
#               buckets. Bad names under a pre-existing bucket are the standing
#               reconcile's job (it can distinguish backlog from fresh; this hook
#               cannot, with a top-level-only snapshot). python3 does the set
#               arithmetic and regex (names may contain spaces).
FINDINGS=$(ACCOUNT_DIR="$ACCOUNT_DIR" SNAP="$SNAP" ALLOWED="$ALLOWED" python3 -c '
import os, sys, re
acct = os.environ["ACCOUNT_DIR"]
try:
    now = set(os.listdir(acct))
except Exception:
    sys.exit(1)
with open(os.environ["SNAP"]) as f:
    before = set(l for l in f.read().split("\n") if l != "")
allowed = set(l.strip() for l in os.environ["ALLOWED"].split("\n") if l.strip())

out = []
for name in sorted((now - before) - allowed):
    out.append("top-level\t" + name)

folder_re = re.compile(r"^[a-z0-9-]+\Z")
file_re = re.compile(r"^[a-z0-9-]+(\.[a-z0-9-]+)*\Z")
for bucket in ("projects", "contacts", "documents"):
    if bucket in before or bucket not in now:
        continue  # pre-existing bucket, or not created this command
    bpath = os.path.join(acct, bucket)
    try:
        children = os.listdir(bpath)
    except Exception:
        continue
    for child in sorted(children):
        if child.startswith("."):
            continue  # dot-prefixed entries are skipped at every level
        rx = folder_re if os.path.isdir(os.path.join(bpath, child)) else file_re
        if not rx.match(child):
            out.append("bad-name\t" + child)

sys.stdout.write("\n".join(out))
' 2>/dev/null) || exit 0

[ -z "$FINDINGS" ] && exit 0

# One log line per finding, then a schema-citing feedback block per reason seen.
# exit 2 feeds stderr back to the agent.
TOPLEVEL=""
BADNAME=""
{
  while IFS=$'\t' read -r reason name; do
    [ -z "$reason" ] && continue
    case "$reason" in
      top-level) echo "[fs-guard-bash] stray path=$name reason=top-level"; TOPLEVEL="$TOPLEVEL $name" ;;
      bad-name)  echo "[fs-guard-bash] stray path=$name reason=bad-name";  BADNAME="$BADNAME $name" ;;
    esac
  done <<< "$FINDINGS"
  if [ -n "$TOPLEVEL" ]; then
    echo "Blocked: this Bash command created top-level entries that are not allowed in this account's data directory:$TOPLEVEL. The layout is fixed by SCHEMA.md — only the entries listed there are permitted at the top level. Relocate this content under projects/, contacts/, or documents/, and route any reorganisation through the data-manager specialist."
  fi
  if [ -n "$BADNAME" ]; then
    echo "Blocked: this Bash command created names that break the account's naming rule:$BADNAME. Folder and file names under projects/, contacts/, and documents/ use lowercase letters, digits, and hyphens only, with a lowercase file extension. Rename them, or route a reorganisation through the data-manager specialist."
  fi
} >&2
exit 2
