#!/usr/bin/env bash
# Orchestrator-agnostic entrypoint for running validate-repo unattended.
#
# Runs the validate-repo skill via the Claude Code CLI in headless (--print)
# mode, writes a markdown report, and — optionally — opens a pull request with
# the report and a second PR with conservative, mechanical fixes.
#
# This script has NO dependency on any particular scheduler. Run it from cron,
# a CI job, an Airflow operator, or by hand. See README.md in this folder.
#
# Required env:
#   ANTHROPIC_API_KEY   Claude API key (the CLI reads it directly under --bare).
#   DBT_PROJECT_DIR     Absolute path to the dbt project root (has dbt_project.yml).
#
# Optional env:
#   PLUGIN_DIR          Path to a checkout/.zip of the gemma-dbt plugin. Passed
#                       as --plugin-dir so the skill is available headless. Omit
#                       if the plugin is already installed for this CLI.
#   REPORT_PATH         Where to write the report
#                       (default: $DBT_PROJECT_DIR/audit/repo-validation-<date>.md).
#   PROMPT_DIR          Folder holding audit-prompt.md / fixes-prompt.md
#                       (default: this script's directory).
#   CLAUDE_MODEL        Override the orchestrator model (e.g. a specific Opus).
#   OPEN_PR             "true" to commit + push + open the report PR (default: false).
#   RUN_FIXES           "true" to run the conservative auto-fixes second pass and
#                       open a fixes PR (requires OPEN_PR=true; default: false).
#   FIXES_MAX_FILES     Safety cap for the auto-fixes pass: if its staged diff
#                       contains ANY deletion or more than this many files, the
#                       fixes PR is discarded (default: 50). See Phase 2.
#
# Required only when OPEN_PR=true:
#   GITHUB_TOKEN        A token that can push and open PRs. Use a PAT, or mint a
#                       GitHub App installation token for a bot identity. The
#                       script assumes `gh` is installed and authenticates it
#                       with this token.
#   GIT_AUTHOR_NAME     Commit author name (e.g. "data-audit-bot").
#   GIT_AUTHOR_EMAIL    Commit author email.
set -euo pipefail

: "${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY must be set}"
: "${DBT_PROJECT_DIR:?DBT_PROJECT_DIR must be set (path to the dbt project root)}"
: "${OPEN_PR:=false}"
: "${RUN_FIXES:=false}"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
: "${PROMPT_DIR:=${SCRIPT_DIR}}"
: "${AUDIT_PROMPT_FILE:=${PROMPT_DIR}/audit-prompt.md}"
: "${FIXES_PROMPT_FILE:=${PROMPT_DIR}/fixes-prompt.md}"

DATE="$(date -u +%F)"
: "${REPORT_PATH:=${DBT_PROJECT_DIR}/audit/repo-validation-${DATE}.md}"
REPORT_BRANCH="audit/repo-validation-${DATE}"
FIXES_BRANCH="audit/repo-fixes-${DATE}"

# Common headless flags. --bare = reproducible minimal mode (no hooks/auto-memory/
# CLAUDE.md auto-discovery; auth strictly via ANTHROPIC_API_KEY). The skill still
# loads via --plugin-dir and resolves through the wrapper prompt below.
build_claude_args() {
  CLAUDE_ARGS=(
    --bare
    --print
    --dangerously-skip-permissions
    --disallowed-tools AskUserQuestion
  )
  [[ -n "${PLUGIN_DIR:-}" ]]   && CLAUDE_ARGS+=(--plugin-dir "${PLUGIN_DIR}")
  [[ -n "${CLAUDE_MODEL:-}" ]] && CLAUDE_ARGS+=(--model "${CLAUDE_MODEL}")
}

# Node/libuv sets O_NONBLOCK on the stdio pipes the `claude` CLI inherits; the
# flag lives on the shared pipe description and OUTLIVES the claude process. When
# an orchestrator captures this script's stdout through a pipe (Prefect, Airflow,
# a CI log collector, `... | tee`), the next large writer — typically git's commit
# summary — hits that non-blocking fd and dies with EAGAIN ("Resource temporarily
# unavailable" -> exit 128). Clear O_NONBLOCK on fds 0/1/2 after each claude run.
restore_blocking_stdio() {
  python3 - <<'PY' || true
import fcntl, os
for fd in (0, 1, 2):
    try:
        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
        fcntl.fcntl(fd, fcntl.F_SETFL, fl & ~os.O_NONBLOCK)
    except OSError:
        pass
PY
}

# --- Optional: set up git + gh for PR creation -----------------------------
if [[ "${OPEN_PR}" == "true" ]]; then
  : "${GITHUB_TOKEN:?GITHUB_TOKEN must be set when OPEN_PR=true}"
  : "${GIT_AUTHOR_NAME:?GIT_AUTHOR_NAME must be set when OPEN_PR=true}"
  : "${GIT_AUTHOR_EMAIL:?GIT_AUTHOR_EMAIL must be set when OPEN_PR=true}"

  cd "${DBT_PROJECT_DIR}"
  # Repo root may be a parent of the dbt project; cd to the git toplevel.
  cd "$(git rev-parse --show-toplevel)"

  echo "${GITHUB_TOKEN}" | gh auth login --with-token
  gh auth setup-git
  # If the remote is SSH, switch to HTTPS so gh's credential helper is used.
  REMOTE_URL="$(git remote get-url origin)"
  if [[ "${REMOTE_URL}" == git@github.com:* ]]; then
    git remote set-url origin "https://github.com/${REMOTE_URL#git@github.com:}"
  fi
  git config user.name "${GIT_AUTHOR_NAME}"
  git config user.email "${GIT_AUTHOR_EMAIL}"

  BASE_SHA="$(git rev-parse HEAD)"
  git checkout -B "${REPORT_BRANCH}"
fi

# --- Phase 1: run the audit, write the report ------------------------------
mkdir -p "$(dirname "${REPORT_PATH}")"

if [[ ! -r "${AUDIT_PROMPT_FILE}" ]]; then
  echo "ERROR: audit prompt not readable: ${AUDIT_PROMPT_FILE}" >&2
  exit 1
fi

export DBT_PROJECT_DIR REPORT_PATH
PROMPT="$(envsubst '$DBT_PROJECT_DIR $REPORT_PATH' < "${AUDIT_PROMPT_FILE}")"

build_claude_args
echo "[run-audit] running validate-repo (typically 2-15 min)..."
claude "${CLAUDE_ARGS[@]}" "${PROMPT}"
restore_blocking_stdio

if [[ ! -s "${REPORT_PATH}" ]]; then
  echo "ERROR: report was not written to ${REPORT_PATH}" >&2
  exit 1
fi
echo "[run-audit] report written: ${REPORT_PATH}"

if [[ "${OPEN_PR}" != "true" ]]; then
  echo "[run-audit] OPEN_PR != true — done (report only)."
  exit 0
fi

# --- Phase 1b: open the report PR ------------------------------------------
git add "${REPORT_PATH}"
git commit -m "chore(audit): dbt repo audit ${DATE}"
git push origin "${REPORT_BRANCH}"
gh pr create \
  --title "dbt repo audit — ${DATE}" \
  --body "Automated dbt repo audit produced by the \`validate-repo\` skill. Report: \`${REPORT_PATH}\`." \
  --label automated,audit
echo "[run-audit] report PR opened."

if [[ "${RUN_FIXES}" != "true" ]]; then
  exit 0
fi

# --- Phase 2 (best-effort): conservative auto-fixes ------------------------
# The validate-repo skill never edits the repo. This is a SEPARATE, narrowly
# scoped pass that applies only mechanical fixes, verifies `dbt parse`, and
# opens a second PR. Every failure path exits 0 so the report PR is unaffected.
if [[ ! -r "${FIXES_PROMPT_FILE}" ]]; then
  echo "[run-audit] fixes prompt not readable — skipping fixes PR." >&2
  exit 0
fi

echo "[run-audit] === Phase 2: conservative auto-fixes ==="
SAVED_REPORT="$(mktemp)"
cp "${REPORT_PATH}" "${SAVED_REPORT}"

# Branch off the original base so the fixes PR carries only code, not the report.
git checkout -B "${FIXES_BRANCH}" "${BASE_SHA}"

export FIXES_REPORT_PATH="${SAVED_REPORT}"
FIXES_PROMPT="$(envsubst '$DBT_PROJECT_DIR $FIXES_REPORT_PATH' < "${FIXES_PROMPT_FILE}")"

build_claude_args
set +e
claude "${CLAUDE_ARGS[@]}" "${FIXES_PROMPT}"
fixes_rc=$?
set -e
restore_blocking_stdio
if [[ "${fixes_rc}" -ne 0 ]]; then
  echo "[run-audit] fixes pass exited ${fixes_rc} — skipping fixes PR." >&2
  exit 0
fi

# Stage only changes inside the dbt project; never the report.
git add -A -- "${DBT_PROJECT_DIR}"
git restore --staged -- "$(dirname "${REPORT_PATH}")" 2>/dev/null || true

if git diff --cached --quiet; then
  echo "[run-audit] no safe auto-fixes produced — no fixes PR."
  exit 0
fi

# Safety guard: the fixes scope only ADDS pk tests and reformats SQL — it never
# deletes files and never touches more than a handful. Any staged deletion, or an
# oversized diff, means the container's working tree drifted from HEAD — a
# .dockerignore/COPY that omitted tracked files (staged as phantom DELETIONS), or
# generated dbt_packages/target/logs not covered by .gitignore (staged as phantom
# ADDITIONS) — so the staged set is untrustworthy. Discard and skip the fixes PR
# rather than risk opening a destructive one (e.g. one that deletes real files).
staged_deletions="$(git diff --cached --name-only --diff-filter=D | wc -l | tr -d ' ')"
staged_total="$(git diff --cached --name-only | wc -l | tr -d ' ')"
if [[ "${staged_deletions}" -gt 0 || "${staged_total}" -gt "${FIXES_MAX_FILES:-50}" ]]; then
  echo "[run-audit] staged diff looks unsafe (${staged_total} files, ${staged_deletions} deletions) — discarding, no fixes PR." >&2
  git reset --quiet -- "${DBT_PROJECT_DIR}"
  git checkout --quiet -- "${DBT_PROJECT_DIR}"
  git clean -fdq -- "${DBT_PROJECT_DIR}"
  exit 0
fi

echo "[run-audit] verifying dbt parse after fixes..."
set +e
( cd "${DBT_PROJECT_DIR}" && dbt parse )
parse_rc=$?
set -e
if [[ "${parse_rc}" -ne 0 ]]; then
  echo "[run-audit] dbt parse failed after fixes — discarding, no fixes PR." >&2
  git reset --quiet -- "${DBT_PROJECT_DIR}"
  git checkout --quiet -- "${DBT_PROJECT_DIR}"
  git clean -fdq -- "${DBT_PROJECT_DIR}"
  exit 0
fi

git commit -m "chore(audit): conservative auto-fixes ${DATE}"
git push origin "${FIXES_BRANCH}"
gh pr create \
  --title "dbt repo audit — auto-fixes — ${DATE}" \
  --body "Conservative, mechanical fixes from the audit report (companion to \`${REPORT_BRANCH}\`). Only missing PK tests and SQL-style reformatting; \`dbt parse\` verified. Logic/renames left for human review." \
  --label automated,audit
echo "[run-audit] fixes PR opened."
