#!/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

# ── Load registry.sh for resolver-based skill emission ────────────────────
# When found, a DISABLED skill's slash-command is suppressed from milestone
# reminders (no dangling ref). When absent, the static slash-command is used.
# Search order: ATOOL_ROOT env → the hook's own sibling lib/ (dev/worktree
# layout) → installed locations — so registry.sh is found without ATOOL_ROOT.
_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_REGISTRY_LOADED=false
for _reg_lib_dir in "${ATOOL_ROOT:-}/lib" "${_HOOK_DIR}/../lib" "$HOME/.claude/lib" "$HOME/.cursor/lib"; do
    if [[ -f "${_reg_lib_dir}/registry.sh" && -f "${_reg_lib_dir}/common.sh" ]]; then
        # shellcheck source=/dev/null
        source "${_reg_lib_dir}/common.sh"
        # shellcheck source=/dev/null
        source "${_reg_lib_dir}/registry.sh"
        _REGISTRY_LOADED=true
        break
    fi
done

# _skill_ref SKILL_NAME — emit "/SKILL_NAME" if it should be referenced, else ""
# (mirrors session-start floor, spec §5.3):
#   registry DATA available + enabled        → "/SKILL_NAME"
#   registry DATA available + disabled/absent → ""  (suppress dangling ref)
#   registry UNAVAILABLE (no lib / no json)  → "/SKILL_NAME"  (static floor)
_skill_ref() {
    local skill_name="$1"
    if $_REGISTRY_LOADED && registry_load 2>/dev/null; then
        if skill_enabled "$skill_name" 2>/dev/null; then
            printf '/%s' "$skill_name"
        fi
    else
        printf '/%s' "$skill_name"
    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")

# Resolve the verification skill reference. If the skill is disabled, $_VBC_REF
# is empty and milestone text uses a generic phrasing (no dangling slash-command).
_VBC_REF=$(_skill_ref "verification-before-completion")
if [[ -n "$_VBC_REF" ]]; then
    _VERIFY_DONE="Run ${_VBC_REF} before claiming done"
    _VERIFY_PROGRESS="Run ${_VBC_REF} to validate progress"
    _VERIFY_REMEMBER="Remember: Run ${_VBC_REF} before claiming done."
else
    _VERIFY_DONE="Complete a verification pass before claiming done"
    _VERIFY_PROGRESS="Complete a verification pass to validate progress"
    _VERIFY_REMEMBER="Remember: complete a verification pass before claiming done."
fi

_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
- ${_VERIFY_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
- ${_VERIFY_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
${_VERIFY_REMEMBER}
</ATOOL-TASK-PROGRESS>"
fi

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

exit 0
