#!/usr/bin/env bash
# AY Framework -- PreToolUse hook for Bash(git commit)
# Verifies that a lock file exists for the active task before allowing commits,
# that the lock is owned by the current session, and that only one commit runs
# at a time (atomic acquisition).
# Outputs JSON: {"decision":"allow"} or {"decision":"deny","message":"..."}
# Works on Mac, Linux, Windows (Git Bash).

set -euo pipefail

# --- Determine the current session id -------------------------------------
# Claude Code delivers the hook payload as JSON on stdin, which includes
# "session_id". Fall back to the CLAUDE_CODE_SESSION_ID env var. If neither is
# available (e.g. hook invoked outside Claude Code), ownership is left
# unverified rather than blocking the commit.
HOOK_PAYLOAD=""
if [ ! -t 0 ]; then
  HOOK_PAYLOAD="$(cat 2>/dev/null || true)"
fi

CURRENT_SESSION="$(printf '%s' "$HOOK_PAYLOAD" \
  | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
  | head -1)"
CURRENT_SESSION="${CURRENT_SESSION:-${CLAUDE_CODE_SESSION_ID:-}}"

# --- Find .ay directory (walk up from cwd) --------------------------------
find_ay_dir() {
  local dir="$PWD"
  while [ "$dir" != "/" ]; do
    if [ -d "$dir/.ay" ]; then
      echo "$dir/.ay"
      return 0
    fi
    dir="$(dirname "$dir")"
  done
  return 1
}

AYF_DIR=$(find_ay_dir 2>/dev/null || echo "")

# If framework not installed, allow (don't block non-framework projects)
if [ -z "$AYF_DIR" ]; then
  echo '{"decision":"allow"}'
  exit 0
fi

# Resolve the locks directory: v1.3+ standard is .ay/tracking/locks;
# fall back to the legacy .ay/locks for older installs.
if [ -d "$AYF_DIR/tracking/locks" ]; then
  LOCK_DIR="$AYF_DIR/tracking/locks"
else
  LOCK_DIR="$AYF_DIR/locks"
fi

# If no locks directory exists, allow (framework installed but no task system active)
if [ ! -d "$LOCK_DIR" ]; then
  echo '{"decision":"allow"}'
  exit 0
fi

# --- Check for exactly one active lock file (*.lock) ----------------------
LOCK_COUNT=0
ACTIVE_TASK=""
ACTIVE_LOCK=""

for lockfile in "$LOCK_DIR"/*.lock; do
  if [ -f "$lockfile" ]; then
    LOCK_COUNT=$((LOCK_COUNT + 1))
    ACTIVE_TASK=$(basename "$lockfile" .lock)
    ACTIVE_LOCK="$lockfile"
  fi
done

if [ "$LOCK_COUNT" -eq 0 ]; then
  echo '{"decision":"deny","message":"No active task lock found. Claim a task before committing: create a lock file in .ay/tracking/locks/{task-name}.lock"}'
  exit 0
fi

if [ "$LOCK_COUNT" -gt 1 ]; then
  echo '{"decision":"deny","message":"Multiple task locks found. Only one task should be active at a time. Clean up .ay/tracking/locks/ first."}'
  exit 0
fi

# ─── Scope Fence (task 61) ───────────────────────────────────────────────────
# Exactly one lock is active. Diff the staged files against the directories/files
# declared for this task and WARN (never block) on anything out of scope, so the
# human can override with intent. Declared scope is read from, in order:
#   1. The active agent's file  .claude/agents/<agent>.md  ("Directories you own")
#   2. The task spec            .ay/tasks/<id>-*.md         (Files/Deliverables/Scope)
# If no scope can be determined, the fence stays silent (backward compatible).

scope_fence() {
  local repo_root
  repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0

  # Task number from the lock name: "task-02" -> "02", "02" -> "02".
  local tnum="${ACTIVE_TASK#task-}"

  # Collect declared scope patterns (directories or files, backtick-quoted in the
  # source docs). Placeholders like src/{{module}}/ are dropped.
  local patterns=()

  # Agent name from the lock body: "agent: core-platform ...".
  local agent_name=""
  if [ -n "$ACTIVE_LOCK" ] && [ -f "$ACTIVE_LOCK" ]; then
    agent_name="$(sed -n 's/.*agent:[[:space:]]*\([A-Za-z0-9._-][A-Za-z0-9._-]*\).*/\1/p' "$ACTIVE_LOCK" | head -n1)"
  fi

  local agent_file="$repo_root/.claude/agents/${agent_name}.md"
  if [ -n "$agent_name" ] && [ -f "$agent_file" ]; then
    while IFS= read -r p; do
      patterns+=("$p")
    done < <(awk '
      /Directories you own/        {grab=1; next}
      /Directories you may read/   {grab=0}
      /Directories you must never/ {grab=0}
      /^## /                       {grab=0}
      grab
    ' "$agent_file" | grep -oE '`[^`]+`' | tr -d '`')
  fi

  # Task spec: Files to Create / Files to Modify / Deliverables / Scope sections.
  local task_file
  task_file="$(ls "$repo_root/.ay/tasks/${tnum}"-*.md 2>/dev/null | head -n1)"
  if [ -n "$task_file" ] && [ -f "$task_file" ]; then
    while IFS= read -r p; do
      patterns+=("$p")
    done < <(awk '
      /^#/ { sec = ($0 ~ /Files to Create|Files to Modify|Deliverable|Scope/) ? 1 : 0 }
      sec
    ' "$task_file" | grep -oE '`[^`]+`' | tr -d '`' | grep -E '/|\.[A-Za-z0-9]+$')
  fi

  # Nothing declared anywhere -> stay silent (backward compatible).
  # (Guard the array length before expanding -- bash 3.2 + set -u errors on
  # "${patterns[@]}" when the array is empty.)
  [ "${#patterns[@]}" -eq 0 ] && return 0

  # Drop template placeholders; if nothing usable remains, stay silent.
  local clean=()
  local p
  for p in "${patterns[@]}"; do
    case "$p" in
      *'{{'*) continue ;;
      '') continue ;;
    esac
    clean+=("$p")
  done
  [ "${#clean[@]}" -eq 0 ] && return 0

  # Compare staged files against the declared scope. .ay/ is always allowed
  # (operational state, gitignored anyway).
  local violations=()
  local f d ok
  while IFS= read -r f; do
    [ -z "$f" ] && continue
    case "$f/" in
      .ay/*) continue ;;
    esac
    ok=0
    for p in "${clean[@]}"; do
      d="${p#./}"; d="${d%/}/"
      case "$f/" in
        "$d"*) ok=1; break ;;
      esac
    done
    [ "$ok" -eq 0 ] && violations+=("$f")
  done < <(git diff --cached --name-only 2>/dev/null)

  if [ "${#violations[@]}" -gt 0 ]; then
    {
      echo "AY Framework scope fence [WARN] -- task '$ACTIVE_TASK'${agent_name:+ (agent: $agent_name)}"
      echo "  These staged files are OUTSIDE the declared scope for this task:"
      for f in "${violations[@]}"; do echo "    - $f"; done
      echo "  Declared scope:"
      for p in "${clean[@]}"; do echo "    * $p"; done
      echo "  Not blocking -- commit proceeds. Re-scope the task or unstage if unintended."
    } >&2
  fi
}

# Exactly one lock file exists -- run the scope fence (warn-only), then allow.
scope_fence || true
echo '{"decision":"allow"}'
