#!/usr/bin/env bash
# scripts/check-architecture-rules.sh
#
# Enforces architecture invariants defined in docs/ARCHITECTURE_RULES.md.
# Exits 1 with a clear message if any forbidden pattern is found in customer-facing paths.
# Run automatically by .github/workflows/architecture.yml on every PR.
#
# Usage:
#   bash scripts/check-architecture-rules.sh        # check all customer-facing files
#   bash scripts/check-architecture-rules.sh --fix  # (not implemented — fix manually)
#
# ── Why every rule below filters prose ──────────────────────────────────────
#
# These rules exist to stop forbidden things from SHIPPING. A pattern named in a
# comment, a docstring, a UI placeholder or a compliance checklist ships nothing —
# it documents policy. Before 2026-08-06 the rules matched raw text, so all three
# fired exclusively on prose: Rule 1 flagged the very checklist that verifies
# Cloudflare's absence, Rule 2 flagged docstrings that say bootstrap.py owns
# thick mode, Rule 3 flagged `placeholder="e.g. EBSDB"`. 9 violations, 0 real.
# A check that cannot be satisfied gets switched off, so each rule now matches
# code positions only.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RULES_DOC="docs/ARCHITECTURE_RULES.md"

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'; BOLD='\033[1m'

# ── Scan scope: git-tracked files ONLY ───────────────────────────────────────
#
# These rules govern what SHIPS, and only tracked files ship. Walking the working
# tree instead swept in .claude/worktrees/ — five full clones of the repo, 3.7 GB —
# so the check took >2 minutes and reported 8 phantom violations from copies of
# files that had already been fixed on main. It reported FAILED for reasons no
# reviewer could act on, which is worse than not running.
#
# git ls-files also drops node_modules/, .git/, __pycache__/ and any scratch file
# for free, so the per-rule exclusion lists below only need to cover files that
# are genuinely tracked and genuinely allowed to mention the pattern.
if ! git -C "${REPO_ROOT}" rev-parse --git-dir >/dev/null 2>&1; then
  echo -e "${RED}Not a git repository: ${REPO_ROOT}${NC}" >&2
  exit 2
fi

# Newline-separated, repo-root-relative. Filtered per-rule by extension.
TRACKED_FILES="$(git -C "${REPO_ROOT}" ls-files)"

# grep over tracked files matching the given extensions.
#   scan_tracked '\.(js|html)$' 'pattern'
scan_tracked() {
  local ext_re="$1" pattern="$2"
  # shellcheck disable=SC2016
  printf '%s\n' "${TRACKED_FILES}" \
    | grep -E "${ext_re}" \
    | tr '\n' '\0' \
    | (cd "${REPO_ROOT}" && xargs -0 -r grep -niE "${pattern}" /dev/null) \
    || true
}

# Drop `path:lineno:` hits whose content begins a comment. Covers #, //, /*, *,
# <!-- and SQL --. A pattern named in a comment documents policy; it ships nothing.
drop_comment_lines() {
  grep -Ev '^[^:]+:[0-9]+:[[:space:]]*(#|//|/\*|\*|<!--|--)' || true
}

# Emit `path:lineno:code` for a Python file with comments, "docstrings" and
# quoted strings blanked, so a match means executable code rather than prose.
# Only """ is handled because agent/ uses no ''' docstrings (verified 2026-08-06).
py_code_lines() {
  local f="$1"
  # SQ is built with sprintf rather than a \047 escape: escape sequences inside a
  # regex literal are not portable, and CI runs mawk, not gawk.
  awk -v path="$f" '
    BEGIN { ind = 0; SQ = sprintf("%c", 39) }
    {
      line = $0
      if (ind) {                                   # continuing a docstring
        p = index(line, "\"\"\"")
        if (p == 0) { print path ":" NR ":"; next }
        line = substr(line, p + 3); ind = 0
      }
      while ((p = index(line, "\"\"\"")) > 0) {    # docstrings opened on this line
        head = substr(line, 1, p - 1)
        rest = substr(line, p + 3)
        q = index(rest, "\"\"\"")
        if (q > 0) { line = head " " substr(rest, q + 3) }
        else       { line = head; ind = 1; break }
      }
      gsub(/"[^"]*"/, " ", line)                   # single-line "strings"
      gsub(SQ "[^" SQ "]*" SQ, " ", line)          # single-line SQ-quoted strings
      sub(/#.*$/, "", line)                        # trailing comments
      print path ":" NR ":" line
    }
  ' "${REPO_ROOT}/$f"
}

report() {   # report <headline> <pattern> <matches> <footer...>
  local headline="$1" pattern="$2" matches="$3"; shift 3
  echo -e "${RED}❌ ${headline}${NC}"
  echo "   Pattern: ${pattern}"
  echo ""
  while IFS= read -r line; do
    echo -e "   ${YELLOW}$(echo "$line" | sed "s|${REPO_ROOT}/||")${NC}"
  done <<< "$matches"
  echo ""
  for l in "$@"; do echo "   $l"; done
  echo ""
}

echo ""
echo -e "${BOLD}Architecture Rules Check${NC}"
echo "  Reference: ${RULES_DOC}"
echo ""

VIOLATIONS=0

# ── Rule 1: No Cloudflare/tunnel in customer-facing paths ──────────────────
#
# Forbidden strings, matched case-insensitively against shipping code.
FORBIDDEN_PATTERNS=(
  "cloudflared"
  "cloudflare-tunnel"
  "tunevault-tunnel\\.service"
  "cf_enabled"
  "cf_tunnel"
  "trycloudflare"
  "cfargotunnel\\.com"
)

# Files allowed to name these patterns because enumerating them IS their job:
#   - docs/ARCHITECTURE_RULES.md      states the rule
#   - this script                     implements it
#   - docs/installer/wave1-matrix.md  records the grep proving install.sh is clean
#   - scripts/personal-lab/**         dev-only, never shipped
#   - docs/internal/**                internal notes
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
  matches=$(scan_tracked '\.(js|html|sh|py|ts|md|yml|yaml)$' "$pattern" \
    | grep -v "package-lock\.json" \
    | grep -v "scripts/check-architecture-rules\.sh" \
    | grep -v "docs/ARCHITECTURE_RULES\.md" \
    | grep -v "docs/installer/wave1-matrix\.md" \
    | grep -v "scripts/personal-lab/" \
    | grep -v "docs/internal/" \
    | drop_comment_lines \
    || true)

  if [ -n "$matches" ]; then
    report "Cloudflare/tunnel reference found (Rule 1)" "$pattern" "$matches" \
      "TuneVault's agent is outbound-only long-poll. No tunnels, no inbound ports." \
      "See: ${RULES_DOC}"
    VIOLATIONS=$((VIOLATIONS + 1))
  fi
done

# ── Rule 2: Thick-mode Oracle client stays inside agent/bootstrap.py ────────
#
# REVISED 2026-08-06. The old rule asserted "v6 uses zero Oracle client" and
# scanned agent/ plus install-v6.sh — a path that no longer exists. That premise
# is dead: agent/bootstrap.py deliberately downloads Instant Client and calls
# init_oracle_client() as a documented thick-mode fallback (DPY-3015 / ORA-28040
# / 11g verifier). Enforcing the dead premise flagged bootstrap.py for doing its
# job, so docs/ci-required-checks.md recorded the rule as stale and ungateable.
#
# The invariant that IS true — and that agent/db.py's own docstring states
# ("Does NOT own: ... thick-mode bootstrap") — is that thick-mode setup is
# CENTRALISED. bootstrap.py owns it; every other agent module stays thin.
THICK_MODE_PATTERNS=(
  "ORACLE_HOME"
  "libclntsh"
  "init_oracle_client"
)
THICK_MODE_OWNER="agent/bootstrap.py"

AGENT_PY=$(printf '%s\n' "${TRACKED_FILES}" | grep -E '^agent/.*\.py$' | grep -v "^${THICK_MODE_OWNER}$" || true)

for pattern in "${THICK_MODE_PATTERNS[@]}"; do
  matches=""
  while IFS= read -r f; do
    [ -n "$f" ] || continue
    hit=$(py_code_lines "$f" | grep -iE ":[0-9]+:.*${pattern}" || true)
    [ -n "$hit" ] && matches="${matches}${hit}"$'\n'
  done <<< "$AGENT_PY"
  matches=$(printf '%s' "$matches" | grep -v '^$' || true)

  if [ -n "$matches" ]; then
    report "Thick-mode Oracle client outside ${THICK_MODE_OWNER} (Rule 2)" "$pattern" "$matches" \
      "Instant Client bootstrap is centralised in ${THICK_MODE_OWNER}." \
      "Other agent modules must stay thin — call bootstrap.ensure_thick_client() instead."
    VIOLATIONS=$((VIOLATIONS + 1))
  fi
done

# The v6 driver guarantee that still holds everywhere in agent/, bootstrap included.
LEGACY_DRIVER_PATTERNS=(
  "cx_Oracle"
  "LD_LIBRARY_PATH"
)

for pattern in "${LEGACY_DRIVER_PATTERNS[@]}"; do
  matches=""
  while IFS= read -r f; do
    [ -n "$f" ] || continue
    hit=$(py_code_lines "$f" | grep -iE ":[0-9]+:.*${pattern}" || true)
    [ -n "$hit" ] && matches="${matches}${hit}"$'\n'
  done <<< "$(printf '%s\n' "${TRACKED_FILES}" | grep -E '^agent/.*\.py$' || true)"
  matches=$(printf '%s' "$matches" | grep -v '^$' || true)

  if [ -n "$matches" ]; then
    report "Legacy Oracle driver dependency in agent/ (Rule 2)" "$pattern" "$matches" \
      "agent/ uses python-oracledb. cx_Oracle and LD_LIBRARY_PATH shims are not permitted."
    VIOLATIONS=$((VIOLATIONS + 1))
  fi
done

# ── Rule 3: No lab artifacts, and no example SID as a runtime default ──────
#
# REVISED 2026-08-06. The old rule banned the strings "ebsdb"/"ebscdb" outright,
# on the theory that they could only have come from a dev lab. They are also the
# generic names for an EBS database, so the ban fired on 15 lines of UI
# placeholders ("e.g. EBSDB or ORCL"), the deliberate is_demo fixture, the
# /sample-report page and unit-test fixtures — and zero real leaks.
#
# Bug #1670982 was a stale SID POISONING probe 5: a lab value used as a live
# default. Mentioning a SID is harmless; defaulting to one is the defect. So:
#   3a — lab-specific hostnames must never appear in shipping code.
#   3b — an example SID must never be a runtime default or fallback.

# 3a: the lab host. Legitimate in docs/ (validation records) and as an enum value
# in installer_validation_runs.os ('ol7'|'ol8'|'ebs12210'), so .sql is out of scope.
LAB_HOST_PATTERNS=(
  "ebs12210-db-dev"
)

for pattern in "${LAB_HOST_PATTERNS[@]}"; do
  matches=$(scan_tracked '\.(js|html|sh|py|ts)$' "$pattern" \
    | grep -v "package-lock\.json" \
    | grep -v "scripts/check-architecture-rules\.sh" \
    | grep -v "^docs/" \
    | grep -v "scripts/personal-lab/" \
    | grep -v "docs/internal/" \
    | drop_comment_lines \
    || true)

  if [ -n "$matches" ]; then
    report "Lab hostname found in shipping code (Rule 3a)" "$pattern" "$matches" \
      "This host exists only in the validation lab. Shipping code must never target it." \
      "See: bug #1670982"
    VIOLATIONS=$((VIOLATIONS + 1))
  fi
done

# 3b: an example SID reached for as a default. Matches the fallback idioms —
# `x || 'EBSDB'`, `x ?? 'EBSDB'`, `x or 'EBSDB'`, `default: 'EBSDB'`,
# `os.environ.get('SID', 'EBSDB')` — and nothing else.
EXAMPLE_SID_PATTERNS=(
  "ebsdb"
  "ebscdb"
)
DEFAULT_CTX='(\|\||\?\?|\bor\b|\bdefault[a-z_]*[[:space:]]*[:=]|\.get\([^)]*,)[[:space:]]*.?[^"'"'"']*'

for pattern in "${EXAMPLE_SID_PATTERNS[@]}"; do
  matches=$(scan_tracked '\.(js|html|sh|py|ts|json)$' "${DEFAULT_CTX}${pattern}" \
    | grep -v "package-lock\.json" \
    | grep -v "scripts/check-architecture-rules\.sh" \
    | grep -v "^tests/" \
    | grep -v "^docs/" \
    | grep -v "public/sample-report\.html" \
    | grep -v "demo-data\.js" \
    | grep -v "todos/" \
    | grep -v "docs/internal/" \
    | grep -viE "demo|e\.g\.|placeholder|example" \
    | drop_comment_lines \
    || true)

  if [ -n "$matches" ]; then
    report "Example SID used as a runtime default (Rule 3b)" "$pattern" "$matches" \
      "Naming a SID is fine; defaulting to one is not — that is how bug #1670982 poisoned probe 5." \
      "Require the value explicitly, or fail loudly when it is absent."
    VIOLATIONS=$((VIOLATIONS + 1))
  fi
done

if [ "$VIOLATIONS" -eq 0 ]; then
  echo -e "${GREEN}✓ All architecture rules pass — zero violations.${NC}"
  echo ""
  exit 0
else
  echo -e "${RED}${BOLD}Architecture check FAILED: ${VIOLATIONS} pattern(s) violated.${NC}"
  echo ""
  echo "  Fix: Remove the forbidden references or move them to:"
  echo "    scripts/personal-lab/  (dev-only scripts)"
  echo "    docs/internal/         (internal notes)"
  echo ""
  echo "  See ${RULES_DOC} for the full rule set."
  echo ""
  exit 1
fi
