#!/usr/bin/env bash
# check-loc.sh — File-size budget enforcer for breakpilot-compliance.
#
# Soft target: 300 LOC. Hard cap: 500 LOC.
#
# Usage:
#   scripts/check-loc.sh                    # scan whole repo, respect exceptions
#   scripts/check-loc.sh --changed          # only files changed vs origin/main
#   scripts/check-loc.sh path/to/file.py    # check specific files
#   scripts/check-loc.sh --json             # machine-readable output
#
# Exit codes:
#   0 — clean (no hard violations)
#   1 — at least one file exceeds the hard cap (500)
#   2 — invalid invocation
#
# Behavior:
#   - Skips test files, generated files, vendor dirs, node_modules, .git, dist, build,
#     .next, __pycache__, migrations, and anything matching .claude/rules/loc-exceptions.txt.
#   - Counts non-blank, non-comment-only lines is NOT done — we count raw lines so the
#     rule is unambiguous. If you want to game it with blank lines, you're missing the point.

set -euo pipefail

SOFT=300
HARD=500
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
EXCEPTIONS_FILE="$REPO_ROOT/.claude/rules/loc-exceptions.txt"

CHANGED_ONLY=0
JSON=0
TARGETS=()

for arg in "$@"; do
  case "$arg" in
    --changed) CHANGED_ONLY=1 ;;
    --json)    JSON=1 ;;
    -h|--help)
      sed -n '2,18p' "$0"; exit 0 ;;
    -*) echo "unknown flag: $arg" >&2; exit 2 ;;
    *)  TARGETS+=("$arg") ;;
  esac
done

# Patterns excluded from the budget regardless of path.
is_excluded() {
  local f="$1"
  case "$f" in
    */node_modules/*|*/.next/*|*/.git/*|*/dist/*|*/build/*|*/__pycache__/*|*/vendor/*) return 0 ;;
    */migrations/*|*/alembic/versions/*) return 0 ;;
    *_test.go|*.test.ts|*.test.tsx|*.spec.ts|*.spec.tsx) return 0 ;;
    *_test.py|*/test_*.py|test_*.py) return 0 ;;
    */tests/*|*/test/*) return 0 ;;
    *.md|*.json|*.yaml|*.yml|*.lock|*.sum|*.mod|*.toml|*.cfg|*.ini) return 0 ;;
    *.html|*.html.j2|*.jinja|*.jinja2) return 0 ;;
    *.svg|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.woff|*.woff2|*.ttf) return 0 ;;
    *.xls|*.xlsx|*.xlsm|*.docx|*.pptx|*.zip|*.tar|*.gz) return 0 ;;
    *.generated.*|*.gen.*|*_pb.go|*_pb2.py|*.pb.go) return 0 ;;
  esac
  return 1
}

is_in_exceptions() {
  [[ -f "$EXCEPTIONS_FILE" ]] || return 1
  local rel="${1#$REPO_ROOT/}"
  grep -Fxq "$rel" "$EXCEPTIONS_FILE"
}

collect_targets() {
  if (( ${#TARGETS[@]} > 0 )); then
    printf '%s\n' "${TARGETS[@]}"
  elif (( CHANGED_ONLY )); then
    git -C "$REPO_ROOT" diff --name-only --diff-filter=AM origin/main...HEAD 2>/dev/null \
      || git -C "$REPO_ROOT" diff --name-only --diff-filter=AM HEAD
  else
    git -C "$REPO_ROOT" ls-files
  fi
}

violations_hard=()
violations_soft=()

while IFS= read -r f; do
  [[ -z "$f" ]] && continue
  abs="$f"
  [[ "$abs" != /* ]] && abs="$REPO_ROOT/$f"
  [[ -f "$abs" ]] || continue
  is_excluded "$abs" && continue
  is_in_exceptions "$abs" && continue
  loc=$(wc -l < "$abs" | tr -d ' ')
  if (( loc > HARD )); then
    violations_hard+=("$loc	$f")
  elif (( loc > SOFT )); then
    violations_soft+=("$loc	$f")
  fi
done < <(collect_targets)

if (( JSON )); then
  printf '{"hard":['
  first=1; for v in "${violations_hard[@]}"; do
    loc="${v%%	*}"; path="${v#*	}"
    (( first )) || printf ','; first=0
    printf '{"loc":%s,"path":"%s"}' "$loc" "$path"
  done
  printf '],"soft":['
  first=1; for v in "${violations_soft[@]}"; do
    loc="${v%%	*}"; path="${v#*	}"
    (( first )) || printf ','; first=0
    printf '{"loc":%s,"path":"%s"}' "$loc" "$path"
  done
  printf ']}\n'
else
  if (( ${#violations_soft[@]} > 0 )); then
    echo "::warning:: $((${#violations_soft[@]})) file(s) exceed soft target ($SOFT lines):"
    printf '  %s\n' "${violations_soft[@]}" | sort -rn
  fi
  if (( ${#violations_hard[@]} > 0 )); then
    echo "::error:: $((${#violations_hard[@]})) file(s) exceed HARD CAP ($HARD lines) — split required:"
    printf '  %s\n' "${violations_hard[@]}" | sort -rn
    echo
    echo "If a file legitimately must exceed $HARD lines (generated code, large data tables),"
    echo "add it to .claude/rules/loc-exceptions.txt with a one-line rationale comment above it."
  fi
fi

(( ${#violations_hard[@]} == 0 ))
