#!/bin/sh
#
# pre-commit: runs before every local commit. Four guards:
#
#   1. Migration immutability — reject commits that modify previously-committed
#      Prisma migration .sql files (drift prevention).
#   2. Stale Prisma client refresh — regenerate when schema.prisma is newer
#      than the generated client.
#   3. Secret scan gate — scan the staged snapshot with gitleaks before bytes
#      enter Git history.
#   4. Typecheck gate — when TypeScript files are staged, run `pnpm typecheck`
#      on the affected workspaces and reject the commit if it fails. Catches
#      missing imports, symbol drift, and type errors BEFORE they reach CI.
#
# The typecheck step is skipped when:
#   - Only non-TS files are staged (docs-only, config-only, migration-only)
#   - DPF_SKIP_TYPECHECK=1 is set (emergency escape hatch — use sparingly; CI
#     will still catch what you let through).
#
# The staged secret scan is skipped when:
#   - DPF_SKIP_SECRET_SCAN=1 skips only the staged gitleaks scan. Use only for
#     verified false positives; CI still scans the PR.
#
# Set up: `git config core.hooksPath .githooks` (done automatically at repo setup).

# ─── Guard 1: migration immutability ────────────────────────────────────────

MIGRATIONS_DIR="packages/db/prisma/migrations"

MODIFIED_MIGRATIONS=$(git diff --cached --name-status | grep "^M" | awk '{print $2}' | grep "^$MIGRATIONS_DIR" | grep "\.sql$")

if [ -n "$MODIFIED_MIGRATIONS" ]; then
  echo ""
  echo "ERROR: Committed migration files cannot be modified."
  echo ""
  echo "The following migration file(s) were already committed and must not be changed:"
  echo "$MODIFIED_MIGRATIONS" | sed 's/^/  /'
  echo ""
  echo "Why: Modifying applied migrations corrupts Prisma's checksum tracking and"
  echo "causes DB drift. Other environments (and production) will break on next migrate."
  echo ""
  echo "What to do instead:"
  echo "  - To fix a past migration:  create a NEW migration that corrects it"
  echo "  - To resolve drift:         npx prisma migrate resolve --applied <name>"
  echo "  - To add missing backfill:  create a new data-only migration"
  echo ""
  exit 1
fi

# ─── Guard 2: stale Prisma client refresh ───────────────────────────────────
# When `git pull` brings in a newer schema.prisma, the generated Prisma client
# in packages/db/generated/client stays at the old shape until someone runs
# `pnpm install` (which fires the postinstall: prisma generate). The
# typecheck below will then fail with "Property X does not exist" errors that
# look like main is broken — they're actually just local staleness.
#
# Detect the drift and regenerate transparently. Fast (~1s) and idempotent.

SCHEMA="packages/db/prisma/schema.prisma"
CLIENT="packages/db/generated/client/client.ts"

if [ -f "$SCHEMA" ] && [ -f "$CLIENT" ] && [ "$SCHEMA" -nt "$CLIENT" ]; then
  echo "[pre-commit] schema.prisma is newer than generated client — regenerating..."
  if ! pnpm --filter @dpf/db exec prisma generate >/tmp/pre-commit-prisma.log 2>&1; then
    echo ""
    echo "ERROR: prisma generate failed. Check /tmp/pre-commit-prisma.log."
    cat /tmp/pre-commit-prisma.log | tail -20
    echo ""
    exit 1
  fi
  echo "[pre-commit] Prisma client refreshed."
fi

# ─── Guard 3: staged secret scan ────────────────────────────────────────────

STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR || true)

if [ -n "$STAGED_FILES" ]; then
  if [ "$DPF_SKIP_SECRET_SCAN" = "1" ]; then
    echo "[pre-commit] DPF_SKIP_SECRET_SCAN=1 - skipping staged gitleaks scan."
  else
    echo "[pre-commit] Staged files present - running gitleaks secret scan..."
    echo "[pre-commit] To skip a verified false positive: DPF_SKIP_SECRET_SCAN=1 git commit ..."
    echo ""

    if ! node scripts/security/run-gitleaks-local.mjs --staged; then
      echo ""
      echo "Commit rejected. Gitleaks found secret-shaped content in the staged snapshot."
      echo "Remove the secret, rotate it if it was real, then stage the sanitized file."
      echo ""
      exit 1
    fi

    echo "[pre-commit] secret scan ok"
  fi
fi

# ─── Guard 5: schema regression ─────────────────────────────────────────────
# When schema.prisma is staged, run the same reorder-tolerant set-based
# check the CI workflow uses (.github/workflows/schema-regression-guard.yml
# → packages/db/scripts/schema-regression-guard.mjs). Catches removed
# fields/attributes/models/enum values BEFORE push so contributors don't
# discover the regression after CI runs the same algorithm.
#
# Tolerates within-model attribute reorders (prisma format quirk that
# tripped PR #1366) and whitespace / column-alignment changes.

STAGED_SCHEMA=$(git diff --cached --name-only --diff-filter=ACMR \
  | grep -E '^packages/db/prisma/schema\.prisma$' || true)

if [ -n "$STAGED_SCHEMA" ]; then
  echo "[pre-commit] schema.prisma staged — running schema regression guard..."

  # Prefer origin/main as the comparison base. Fall back to upstream@{u}
  # if the branch tracks something other than origin/main. Skip if neither
  # is available (e.g. fresh clone with no upstream) — CI is the backstop.
  if git rev-parse --verify origin/main >/dev/null 2>&1; then
    SCHEMA_GUARD_BASE="origin/main"
  elif git rev-parse --verify '@{u}' >/dev/null 2>&1; then
    SCHEMA_GUARD_BASE="@{u}"
  else
    SCHEMA_GUARD_BASE=""
  fi

  if [ -n "$SCHEMA_GUARD_BASE" ]; then
    if ! node packages/db/scripts/schema-regression-guard.mjs "$SCHEMA_GUARD_BASE"; then
      echo ""
      echo "Commit rejected. See the regressions above."
      echo "If the removal is intentional, coordinate with the schema steward"
      echo "(AGENTS.md §11) and add an explicit migration plan."
      echo ""
      exit 1
    fi
  else
    echo "[pre-commit] no usable base ref for schema regression guard — skipping (CI backstop)."
  fi
fi

# ─── Guard 4: typecheck gate ────────────────────────────────────────────────

if [ "$DPF_SKIP_TYPECHECK" = "1" ]; then
  echo "[pre-commit] DPF_SKIP_TYPECHECK=1 — skipping typecheck gate."
  exit 0
fi

STAGED_TS=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx|mts|cts)$' || true)

if [ -z "$STAGED_TS" ]; then
  exit 0
fi

echo "[pre-commit] TypeScript files staged — running scoped typecheck..."
echo "[pre-commit] To skip in an emergency: DPF_SKIP_TYPECHECK=1 git commit ..."
echo ""

# Map staged paths to the affected pnpm workspace(s). Each workspace has its
# own `typecheck` script (tsc --noEmit). Running only what's affected keeps
# the hook fast on small commits while still catching cross-file errors
# within each workspace.

RUN_WEB=0
RUN_DB=0
RUN_TYPES=0
RUN_VALIDATORS=0
RUN_API_CLIENT=0
RUN_MOBILE=0
RUN_ADP_SERVICE=0

while IFS= read -r file; do
  case "$file" in
    apps/web/*)                   RUN_WEB=1 ;;
    packages/db/*)                RUN_DB=1 ;;
    packages/types/*)             RUN_TYPES=1 ;;
    packages/validators/*)        RUN_VALIDATORS=1 ;;
    packages/api-client/*)        RUN_API_CLIENT=1 ;;
    apps/mobile/*)                RUN_MOBILE=1 ;;
    services/adp/*)               RUN_ADP_SERVICE=1 ;;
  esac
done <<EOF
$STAGED_TS
EOF

# A change in a shared package (db, types, validators) can break web. Run web
# typecheck whenever any upstream workspace is touched.
if [ "$RUN_DB" = "1" ] || [ "$RUN_TYPES" = "1" ] || [ "$RUN_VALIDATORS" = "1" ]; then
  RUN_WEB=1
fi

FAILED=""

run_typecheck() {
  filter="$1"
  label="$2"
  echo "  → $label"
  if ! pnpm --filter "$filter" typecheck >/tmp/pre-commit-tc.log 2>&1; then
    echo ""
    echo "ERROR: typecheck failed for $label"
    echo ""
    cat /tmp/pre-commit-tc.log | tail -40
    echo ""
    FAILED="$FAILED $label"
  fi
}

[ "$RUN_WEB" = "1" ]           && run_typecheck "web"               "apps/web"
[ "$RUN_DB" = "1" ]            && run_typecheck "@dpf/db"           "packages/db"
[ "$RUN_TYPES" = "1" ]         && run_typecheck "@dpf/types"        "packages/types"
[ "$RUN_VALIDATORS" = "1" ]    && run_typecheck "@dpf/validators"   "packages/validators"
[ "$RUN_API_CLIENT" = "1" ]    && run_typecheck "@dpf/api-client"   "packages/api-client"
[ "$RUN_MOBILE" = "1" ]        && run_typecheck "mobile"            "apps/mobile"
[ "$RUN_ADP_SERVICE" = "1" ]   && run_typecheck "dpf-adp-mcp"       "services/adp"

if [ -n "$FAILED" ]; then
  echo ""
  echo "Commit rejected. Fix the typecheck errors above or, for an emergency override:"
  echo "  DPF_SKIP_TYPECHECK=1 git commit ..."
  echo "(CI will still gate the merge either way.)"
  echo ""
  exit 1
fi

echo "[pre-commit] typecheck ok"
exit 0
