#!/usr/bin/env bash
# aTool - hooks/task-state-tracker
# PostToolUse hook: tracks session-level task state for feedback loop
# Records: modified files, doc staleness, verification status, pending actions
# State file: .claude/task-state.json (session-scoped, gitignored)

set -euo pipefail

# Escape string for JSON embedding
escape_for_json() {
    local s="$1"
    s="${s//\\/\\\\}"
    s="${s//\"/\\\"}"
    s="${s//$'\n'/\\n}"
    s="${s//$'\r'/\\r}"
    s="${s//$'\t'/\\t}"
    printf '%s' "$s"
}

# ── Read tool input ───────────────────────────────────────────────────────

INPUT=""
if [[ ! -t 0 ]]; then
    INPUT=$(cat)
fi

TOOL_NAME=""
FILE_PATH=""
if command -v jq &>/dev/null && [[ -n "$INPUT" ]]; then
    TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || echo "")
    FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")
fi

# Only respond to Write/Edit
if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
    exit 0
fi
if [[ -z "$FILE_PATH" ]]; then
    exit 0
fi

# ── Source file detection ────────────────────────────────────────────────

FILE_EXT="${FILE_PATH##*.}"
FILE_BASE=$(basename "$FILE_PATH")

# Skip non-source files (same logic as doc-sync-reminder)
case "$FILE_EXT" in
    md|markdown|txt|rst) exit 0 ;;
    json|yaml|yml|toml|xml) exit 0 ;;
    css|scss|less|sass|styl) exit 0 ;;
    lock|map|log) exit 0 ;;
    svg|png|jpg|jpeg|gif|ico|webp|ttf|woff|woff2|eot) exit 0 ;;
esac
case "$FILE_BASE" in
    .*|*.config.*) exit 0 ;;
esac
case "$FILE_EXT" in
    ts|tsx|js|jsx|vue|svelte|html|rs|py|go|java|kt|kts|swift|dart|ets|sh|bash) ;;
    *) exit 0 ;;
esac

# ── State management ──────────────────────────────────────────────────────

# Require jq (state tracking uses JSON mutation which grep cannot do)
if ! command -v jq &>/dev/null; then
    printf '[task-state-tracker] jq not installed — task state tracking disabled. Install jq to enable.\n' >&2
    exit 0
fi

NOW=$(date +%s 2>/dev/null || echo "0")
STATE_FILE=".claude/task-state.json"
MAX_TRACKED_FILES=50
# v1.10.13 self-rotation (P2 #15): if the state file is older than this window,
# treat it as a stale leftover from a previous session and archive it before
# starting fresh. CC's "Stop" event fires per-response (not per-session), so
# a Stop hook would clear in-session state — wrong granularity. CC also has
# no SessionEnd. Time-based rotation is the cleanest cross-IDE option.
SESSION_STALE_SECONDS=14400   # 4 hours

# Rotate stale state file before initializing/reading.
if [[ -f "$STATE_FILE" ]]; then
    # GNU stat (-c) on Linux, BSD stat (-f) on macOS. Either may work; pick
    # the first that returns a non-empty number.
    file_mtime=$(stat -c %Y "$STATE_FILE" 2>/dev/null || stat -f %m "$STATE_FILE" 2>/dev/null || echo 0)
    age=$((NOW - file_mtime))
    if [[ "$file_mtime" -gt 0 ]] && [[ "$age" -gt "$SESSION_STALE_SECONDS" ]]; then
        # Archive with timestamp; keep last 5 to bound disk growth.
        mv "$STATE_FILE" "${STATE_FILE}.archived.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
        ls -t "$STATE_FILE".archived.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true
    fi
fi

# Initialize state file if needed
if [[ ! -f "$STATE_FILE" ]]; then
    mkdir -p ".claude" 2>/dev/null || true
    printf '{"session_id":"","modified_files":[],"docs_stale":false,"verification_passed":false,"pending_actions":[],"architecture_violations":0,"created_at":%s,"updated_at":%s}\n' "$NOW" "$NOW" > "$STATE_FILE"
fi

# v1.10.16 (P1-CON-7): mkdir-based mutex + bounded retry. PostToolUse fires
# on every Write/Edit; a tool that does 5 rapid edits to different files
# triggers 5 parallel hook instances, each doing read→jq→write on STATE_FILE.
# Without locking, last-write-wins → 3 of 5 modified_files entries silently
# vanish. We retry briefly (max 1s) and if we still can't get the lock we
# give up cleanly — hooks must NEVER block the IDE on contention.
LOCK_DIR="${STATE_FILE}.lock"
_acquired=0
for _try in 1 2 3 4 5 6 7 8 9 10; do
    if mkdir "$LOCK_DIR" 2>/dev/null; then
        _acquired=1
        break
    fi
    # Stale lock detection (in case a previous instance crashed)
    if [[ -d "$LOCK_DIR" ]] && [[ -f "$LOCK_DIR/ts" ]]; then
        _age=$((NOW - $(cat "$LOCK_DIR/ts" 2>/dev/null || echo "$NOW")))
        if (( _age > 30 )); then
            rm -rf "$LOCK_DIR" 2>/dev/null || true
            continue
        fi
    fi
    sleep 0.1
done
if [[ "$_acquired" != "1" ]]; then
    # Lock contention — bail out without writing. Next hook fire will catch
    # up since the underlying file modification was a Write/Edit (already
    # observable to the IDE), not us.
    exit 0
fi
# shellcheck disable=SC2064
trap "rm -rf '$LOCK_DIR' 2>/dev/null || true" EXIT INT TERM
printf '%s\n' "$$" > "$LOCK_DIR/pid"
printf '%s\n' "$NOW" > "$LOCK_DIR/ts"

# Read current state (now under lock, no torn-write window)
STATE=$(cat "$STATE_FILE" 2>/dev/null || echo "{}")

# Update: add file, set docs_stale=true, reset verification, update timestamp
NEW_STATE=$(printf '%s' "$STATE" | jq \
    --arg fp "$FILE_PATH" \
    --argjson now "$NOW" \
    '
    # Add file if not already tracked
    if (.modified_files | map(select(.path == $fp)) | length) == 0 then
        .modified_files += [{"path": $fp, "modified_at": $now}]
    else
        .modified_files = [.modified_files[] | if .path == $fp then .modified_at = $now else . end]
    end
    # Cap modified files to 50
    | .modified_files = (.modified_files | sort_by(-.modified_at) | .[0:50])
    # Mark docs as stale
    | .docs_stale = true
    # Reset verification since we made changes
    | .verification_passed = false
    # Ensure pending_actions includes doc update
    | if (.pending_actions | index("update_docs") | not) then
        .pending_actions += ["update_docs"]
      else . end
    | .updated_at = $now
    ')

# v1.10.16: stage→mv atomic write (siblings of STATE_FILE → same FS). The
# previous code did `jq '.' > "$STATE_FILE"` which truncated first, leaving
# a 0-byte window; combined with the missing lock above, this was the root
# cause of intermittent state loss under heavy editing.
_STAGE="${STATE_FILE}.tmp.$$"
if printf '%s' "$NEW_STATE" | jq '.' > "$_STAGE" 2>/dev/null; then
    mv "$_STAGE" "$STATE_FILE"
else
    rm -f "$_STAGE" 2>/dev/null || true
fi

# ── Output feedback at milestones ─────────────────────────────────────────

FILE_COUNT=$(jq '.modified_files | length' "$STATE_FILE" 2>/dev/null || echo "0")

_MILESTONE=""
# Milestone check: for every 5 files >= 10, show progress update
if (( FILE_COUNT >= 10 && FILE_COUNT % 5 == 0 )); then
    if (( FILE_COUNT == 10 )); then
        _MILESTONE="<ATOOL-TASK-PROGRESS>
Progress check: You have modified ${FILE_COUNT} source files. This is substantial.
- Consider breaking your work into smaller, verifiable units
- Docs are STALE and need updating
- Run /verification-before-completion before claiming done
</ATOOL-TASK-PROGRESS>"
    else
        _MILESTONE="<ATOOL-TASK-PROGRESS>
Progress check: You have modified ${FILE_COUNT} source files.
- This is a large refactoring. Consider intermediate commits/verification
- Docs are STALE and need updating
- Run /verification-before-completion to validate progress
</ATOOL-TASK-PROGRESS>"
    fi
elif (( FILE_COUNT == 5 )); then
    _MILESTONE="<ATOOL-TASK-PROGRESS>
Progress check: You have modified ${FILE_COUNT} source files.
- Docs are STALE and need updating
- Verification has NOT been run since last changes
Remember: Run /verification-before-completion before claiming done.
</ATOOL-TASK-PROGRESS>"
fi

if [[ -n "$_MILESTONE" ]]; then
    _ESCAPED=$(escape_for_json "$_MILESTONE")
    printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$_ESCAPED"
fi

exit 0
