#!/usr/bin/env bash
# bin/ayf-drift-check -- post-build plan drift detector
#
# Compares the files a task DECLARED it would produce (parsed from the task
# markdown) against filesystem/git reality, and reports drift.
#
# Usage:
#   ayf-drift-check <task-file.md> [--base <git-ref>] [--json]
#
# Declared files are read from the task's "## Files to Create",
# "## Files to Modify", or "## Deliverables" section -- any backtick-wrapped
# token in those sections that looks like a path (contains "/" or a .ext).
#
# Reality:
#   MATCHED  declared AND exists on disk
#   MISSING  declared but NOT on disk (or not among the changed set for --base)
#   EXTRA    changed in git but NOT declared (best-effort; needs --base or a
#            dirty working tree to be meaningful)
#
# Output is human-readable and machine-parseable: one "LABEL<TAB>path" per line.
# Exits non-zero (2) when any drift (MISSING or EXTRA) is detected.
set -euo pipefail

PROG="$(basename "$0")"

die() { echo "$PROG: $*" >&2; exit 64; }

usage() {
  sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
  exit "${1:-0}"
}

# ---- parse args --------------------------------------------------------------
TASK_FILE=""
BASE_REF=""
JSON=0
while [[ $# -gt 0 ]]; do
  case "$1" in
    -h|--help) usage 0 ;;
    --base) BASE_REF="${2:-}"; [[ -n "$BASE_REF" ]] || die "--base requires a git ref"; shift 2 ;;
    --json) JSON=1; shift ;;
    -*) die "unknown option: $1" ;;
    *) [[ -z "$TASK_FILE" ]] || die "unexpected argument: $1"; TASK_FILE="$1"; shift ;;
  esac
done

[[ -n "$TASK_FILE" ]] || usage 64
[[ -f "$TASK_FILE" ]] || die "task file not found: $TASK_FILE"

# Absolute path to the task file before we cd to repo root.
TASK_FILE_ABS="$(cd "$(dirname "$TASK_FILE")" && pwd)/$(basename "$TASK_FILE")"

# All declared/actual paths are resolved relative to the repo root.
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git repository"
cd "$REPO_ROOT"

# Path of the task file relative to repo root (so we can exclude it from EXTRA).
TASK_REL="${TASK_FILE_ABS#"$REPO_ROOT"/}"

# ---- extract declared files --------------------------------------------------
# Grab lines inside the relevant sections, pull backtick tokens, keep the
# path-like ones (contain "/" or end in a dotted extension), drop anything with
# whitespace (those are commands/prose, not paths).
declared="$(
  awk '
    /^##[[:space:]]/ {
      insec = ($0 ~ /Files to Create/ || $0 ~ /Files to Modify/ || $0 ~ /Deliverables/) ? 1 : 0
      next
    }
    insec { print }
  ' "$TASK_FILE_ABS" \
  | grep -oE '`[^`]+`' \
  | tr -d '`' \
  | awk '!/[[:space:]]/ && ($0 ~ /\// || $0 ~ /\.[A-Za-z0-9]+$/)' \
  | sort -u || true
)"

# ---- compute the "actually changed" set (for EXTRA) --------------------------
if [[ -n "$BASE_REF" ]]; then
  git rev-parse --verify -q "$BASE_REF" >/dev/null || die "unknown git ref: $BASE_REF"
  changed="$( { git diff --name-only "$BASE_REF" -- .; git ls-files --others --exclude-standard; } | sort -u )"
else
  # Uncommitted work: staged + unstaged + untracked. Handles rename "old -> new".
  changed="$( git status --porcelain | sed -E 's/^...//; s/.* -> //' | sort -u )"
fi

# Files we never count as EXTRA: operational/tracking state and the task file.
is_ignored() {
  case "$1" in
    "$TASK_REL") return 0 ;;
    .ay/tracking/*|.ay/locks/*|.ay/plans/*|.ay/retros/*|.ay/reviews/*|.ay/sessions/*) return 0 ;;
    *) return 1 ;;
  esac
}

# ---- classify ----------------------------------------------------------------
matched=(); missing=(); extra=()

while IFS= read -r f; do
  [[ -n "$f" ]] || continue
  if [[ -n "$BASE_REF" ]]; then
    # With a base ref, "satisfied" means the file was actually created/changed.
    if printf '%s\n' "$changed" | grep -Fxq -- "$f"; then
      matched+=("$f")
    else
      missing+=("$f")
    fi
  else
    # Without a base ref, "satisfied" means the file exists on disk.
    if [[ -e "$f" ]]; then
      matched+=("$f")
    else
      missing+=("$f")
    fi
  fi
done <<< "$declared"

while IFS= read -r f; do
  [[ -n "$f" ]] || continue
  is_ignored "$f" && continue
  printf '%s\n' "$declared" | grep -Fxq -- "$f" && continue
  extra+=("$f")
done <<< "$changed"

# ---- report ------------------------------------------------------------------
drift=0
(( ${#missing[@]} > 0 || ${#extra[@]} > 0 )) && drift=1

if (( JSON )); then
  json_arr() { local first=1; printf '['; for x in "$@"; do (( first )) || printf ','; first=0; printf '"%s"' "$x"; done; printf ']'; }
  printf '{"task":"%s","matched":%s,"missing":%s,"extra":%s,"drift":%s}\n' \
    "$TASK_REL" \
    "$(json_arr ${matched[@]+"${matched[@]}"})" \
    "$(json_arr ${missing[@]+"${missing[@]}"})" \
    "$(json_arr ${extra[@]+"${extra[@]}"})" \
    "$( ((drift)) && echo true || echo false )"
else
  echo "drift-check: $TASK_REL"
  [[ -n "$BASE_REF" ]] && echo "  (comparing against git ref: $BASE_REF)"
  echo ""
  for f in ${matched[@]+"${matched[@]}"}; do printf 'MATCHED\t%s\n' "$f"; done
  for f in ${missing[@]+"${missing[@]}"}; do printf 'MISSING\t%s\n' "$f"; done
  for f in ${extra[@]+"${extra[@]}"};     do printf 'EXTRA\t%s\n'   "$f"; done
  echo ""
  echo "Summary: ${#matched[@]} matched, ${#missing[@]} missing, ${#extra[@]} extra"
  if (( drift )); then
    echo "STATUS: DRIFTED"
  else
    echo "STATUS: COMPLETE"
  fi
fi

exit $(( drift ? 2 : 0 ))
