#!/usr/bin/env bash
# check-rules.sh — project rule audit engine.
#
# Usage:
#   bash scripts/check-rules.sh           # full audit (verbose, includes slow checks)
#   bash scripts/check-rules.sh --fast    # fast audit (quiet, skips slow checks) — for pre-commit
#   bash scripts/check-rules.sh --quiet   # full audit, only prints failures
#
# Exit code: 0 = all rules pass (warnings are non-fatal), 1 = one or more FAILED.
#
# This is a generalized version of the rules engine proven in the
# RCEO-PersonalAssistant project. Add project-specific checks by appending
# `check "<desc>" "<pass|warn|fail>"` calls below.
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"

# --- argument parsing ---
RULE_FAST=false
RULE_VERBOSE=true
for arg in "$@"; do
    case "$arg" in
        --fast)  RULE_FAST=true; RULE_VERBOSE=false ;;
        --quiet) RULE_VERBOSE=false ;;
        *) die "check-rules.sh: unknown argument '$arg'" ;;
    esac
done
export RULE_FAST RULE_VERBOSE

init_counters
$RULE_VERBOSE && echo "=== Project Rule Audit ==="

TODAY="$(date +%Y-%m-%d)"

# ----------------------------------------------------------------------------
# 1. Shellcheck — every .sh/.bash must pass (zero warnings, incl. info-level).
#    Runs in Docker so the host stays clean (no native shellcheck required).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Shell scripts (shellcheck)"
mapfile -d '' SH_FILES < <(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./.crush -prune -o -path ./vendor -prune -o -path ./oam/librenms-agent/agent -prune -o -path ./archive -prune -o -path ./node_modules -prune -o -path ./legacy-tsys-ca -prune -o \( -name '*.sh' -o -name '*.bash' \) -print0 2>/dev/null)
if [ "${#SH_FILES[@]}" -gt 0 ]; then
    if have shellcheck; then
        if shellcheck "${SH_FILES[@]}" >/dev/null 2>&1; then
            check "All shell scripts pass shellcheck (host)" "pass"
        else
            check "shellcheck reports violations — run: shellcheck <file>" "fail"
        fi
    elif have docker; then
        MNT_FILES=()
        for f in "${SH_FILES[@]}"; do MNT_FILES+=("/mnt/${f#./}"); done
        if docker run --rm -v "$REPO_ROOT:/mnt" koalaman/shellcheck:stable "${MNT_FILES[@]}" >/dev/null 2>&1; then
            check "All shell scripts pass shellcheck (docker)" "pass"
        else
            check "shellcheck (docker) reports violations" "fail"
        fi
    else
        check "No shellcheck or docker available to lint scripts" "warn"
    fi
else
    check "No shell scripts to lint" "pass"
fi

# ----------------------------------------------------------------------------
# 2. Docker image pinning — no ':latest' tags in compose or Dockerfiles.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Docker image pinning"
if grep -rqE '(image:|FROM).*:latest' --include='docker-compose*.y*ml' --include='Dockerfile*' . 2>/dev/null; then
    check "No ':latest' image tags (pin everything)" "fail"
else
    check "No ':latest' image tags" "pass"
fi

# ----------------------------------------------------------------------------
# 2b. Container naming — every service in a docker-compose file MUST set an
#     explicit container_name (never rely on Docker's default <dir>_<n>).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Container naming"
COMPOSE_FILES="$(find . -path ./.git -prune -o \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) -print 2>/dev/null || true)"
if [ -n "$COMPOSE_FILES" ]; then
    BAD=0
    while IFS= read -r cf; do
        [ -n "$cf" ] || continue
        # Count top-level service keys (2-space indent under services:) and
        # compare against the number of container_name: declarations.
        svc_count=$(awk '/^services:/{f=1;next} f&&/^[^[:space:]]/{f=0} f&&/^[[:space:]]{2}[[:alnum:]_-]+:[[:space:]]*$/{c++} END{print c+0}' "$cf")
        cn_count=$(grep -cE '^[[:space:]]*container_name:' "$cf" 2>/dev/null || echo 0)
        if [ "${svc_count:-0}" -gt 0 ] && [ "$cn_count" -lt "$svc_count" ]; then
            BAD=$((BAD + 1))
        fi
    done <<EOF
$COMPOSE_FILES
EOF
    if [ "$BAD" -eq 0 ]; then
        check "All compose services set container_name" "pass"
    else
        check "$BAD compose file(s) with services missing container_name" "fail"
    fi
else
    check "No compose files (container-name check skipped)" "pass"
fi

# ----------------------------------------------------------------------------
# 3. Required-files manifest — the files every project using this template owns.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Required files"
REQUIRED_FILES="AGENTS.md STATUS.md .env.example scripts/check-rules.sh scripts/setup-hooks.sh"
REQUIRED_FILES="$REQUIRED_FILES ${PROJECT_REQUIRED_FILES:-}"
for f in $REQUIRED_FILES; do
    if [ -f "$f" ]; then check "$f exists" "pass"; else check "$f MISSING" "fail"; fi
done
if compgen -G 'questions-v*.md' > /dev/null; then
    latest_q="$(find . -maxdepth 1 -name 'questions-v*.md' -printf '%f\n' | sort -V | tail -1)"
    check "questions-v*.md exists ($latest_q)" "pass"
else
    check "questions-v*.md MISSING" "fail"
fi

# ----------------------------------------------------------------------------
# 4. Doc freshness — STATUS.md touched today.
#    Warning (not failure): staleness is a signal, not a break.
#    Redmine is the system of record for work; Discourse for docs. STATUS.md is
#    a scratchpad only — see BASELINE-PROMPT.md §3, §8.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Doc freshness"
if [ -f STATUS.md ]; then
    STATUS_DATE="$(grep -oE 'Last updated: [0-9]{4}-[0-9]{2}-[0-9]{2}' STATUS.md | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo unknown)"
    if [ "$STATUS_DATE" = "$TODAY" ]; then
        check "STATUS.md updated today ($STATUS_DATE)" "pass"
    else
        check "STATUS.md is stale (last: $STATUS_DATE, today: $TODAY) — update it" "warn"
    fi
else
    check "STATUS.md MISSING" "fail"
fi

# ----------------------------------------------------------------------------
# 4b. Discourse pointer-header policy (MANDATORY).
#     Discourse is the system of record for documentation. In-repo .md files are
#     stubs that point to a Discourse topic URL. Operational files exempt.
#     Override exemptions via PROJECT_DOC_EXEMPT (space-separated globs of
#     basenames) and the Discourse host via PROJECT_DISCOURSE_HOST.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Discourse pointer-header"
DISCOURSE_HOST="${PROJECT_DISCOURSE_HOST:-community.turnsys.com}"
DOC_EXEMPT="${PROJECT_DOC_EXEMPT:-AGENTS.md STATUS.md WORKING.md README.md ADOPTING.md LICENSE .env.example questions-v*.md BASELINE-PROMPT.md PATTERNS.md}"
POINTER_MISSING=0
while IFS= read -r -d '' f; do
    base="$(basename "$f")"
    exempt=false
    for pat in $DOC_EXEMPT; do
        # shellcheck disable=SC2254
        case "$base" in $pat) exempt=true; break ;; esac
    done
    [ "$exempt" = true ] && continue
    if ! grep -qF "$DISCOURSE_HOST" "$f" 2>/dev/null; then
        if [ "$POINTER_MISSING" -eq 0 ]; then
            $RULE_VERBOSE && printf '  %s\n' "Missing $DISCOURSE_HOST URL in:"
        fi
        POINTER_MISSING=$((POINTER_MISSING + 1))
        $RULE_VERBOSE && printf '    %s\n' "$f"
    fi
done < <(find . -path ./.git -prune -o -path ./.crush -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./archive -prune -o -path "./legacy-*" -prune -o -name "*.md" -print0 2>/dev/null)
if [ "$POINTER_MISSING" -eq 0 ]; then
    check "All non-exempt .md cite Discourse ($DISCOURSE_HOST)" "pass"
else
    check "$POINTER_MISSING .md file(s) missing Discourse pointer (see BASELINE-PROMPT.md §3)" "fail"
fi

# ----------------------------------------------------------------------------
# 5. Git state — uncommitted changes are a warning (the pre-push hook hardens
#    this where it matters).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Git state"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    if git diff --quiet && git diff --cached --quiet; then
        check "Working tree clean" "pass"
    else
        check "Uncommitted changes present" "warn"
    fi
else
    check "Not a git repo (git checks skipped)" "pass"
fi

# ----------------------------------------------------------------------------
# 6. Hooks installed — self-check that git hooks were set up.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Git hooks"
if [ -f .git/hooks/pre-commit ]; then
    check "pre-commit hook installed" "pass"
else
    check "pre-commit NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
fi
if [ -f .git/hooks/pre-push ]; then
    check "pre-push hook installed" "pass"
else
    check "pre-push NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
fi

# ----------------------------------------------------------------------------
# 7. WORKING.md completion — no unchecked tasks may remain at commit time.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Task completion"
if [ -f WORKING.md ]; then
    UNCHECKED="$(grep -cF -- '- [ ]' WORKING.md || true)"
    if [ "$UNCHECKED" -eq 0 ]; then
        check "WORKING.md has no unchecked tasks" "pass"
    else
        check "WORKING.md has ${UNCHECKED} unchecked task(s) — finish them before committing" "fail"
    fi
else
    check "WORKING.md absent (no active task tracker)" "pass"
fi

# ----------------------------------------------------------------------------
# 8. CNW markers — empty `CNW:` markers flag unresolved questions for the human.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Unresolved questions"
EMPTY_CNW="$(grep -rn 'CNW:$' . --include='*.md' 2>/dev/null | head -20 || true)"
if [ -z "$EMPTY_CNW" ]; then
    check "No empty CNW: markers (unresolved questions)" "pass"
else
    CNW_COUNT="$(printf '%s\n' "$EMPTY_CNW" | grep -c . || true)"
    check "${CNW_COUNT} unresolved CNW: marker(s) — needs user input" "warn"
fi

# ----------------------------------------------------------------------------
# 9. Hygiene — merge-conflict markers and trailing whitespace must never land.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "File hygiene"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    # archive/ excluded: legacy files are preserved verbatim and may contain
    # decorative `====` banners that false-positive as conflict markers.
    CONFLICT="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -v '^archive/' | xargs -r grep -lE '^(<{7}( \S+)?|=======|>{7}( \S+)?)$' 2>/dev/null || true)"
    if [ -z "$CONFLICT" ]; then check "No merge-conflict markers staged" "pass"; else check "Merge-conflict markers staged: $CONFLICT" "fail"; fi
fi

# ----------------------------------------------------------------------------
# 10. (slow, skipped in --fast) Project test suite via scripts/test.sh.
# ----------------------------------------------------------------------------
if [ "$RULE_FAST" = false ] && [ -x scripts/test.sh ]; then
    $RULE_VERBOSE && log_step "Test suite (scripts/test.sh)"
    if bash scripts/test.sh >/dev/null 2>&1; then
        check "scripts/test.sh passes" "pass"
    else
        check "scripts/test.sh FAILS" "fail"
    fi
fi

# ----------------------------------------------------------------------------
# 11. Access-channel policy — qemu guest-agent must NEVER be used as an
#     execution or key-delivery channel. SSH (sshd) is the ONLY approved
#     remote access path; all commands must be logged through standard
#     auth/audit infrastructure. ITAR/CMMC environment — non-negotiable.
#     Allowed: installing/checking qemu-guest-agent for Proxmox state
#     visibility. Forbidden: `qm guest exec` + any `vm-guest` wrapper.
#     Scans CODE only (not .md) so docs may describe the ban.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Access-channel policy (no guest-agent exec)"
GA_HITS="$(grep -rnE 'qm guest exec|vm-guest|_vm_guest' \
    --include='*.sh' --include='*.bash' --include='*.py' \
    . 2>/dev/null | grep -vE 'scripts/check-rules\.sh|/archive/' || true)"
if [ -z "$GA_HITS" ]; then
    check "No guest-agent exec / vm-guest access patterns in code" "pass"
else
    GA_COUNT="$(printf '%s\n' "$GA_HITS" | grep -c . || true)"
    $RULE_VERBOSE && printf '%s\n' "$GA_HITS" | sed 's/^/    /'
    check "${GA_COUNT} guest-agent exec / vm-guest reference(s) — SSH-only access policy (AGENTS.md)" "fail"
fi

print_summary_and_exit
