# BreakPilot Compliance — CI Pipeline
#
# Feature branch workflow:
#   feat/* | feature/* | fix/* | hotfix/* | chore/* | refactor/* | docs/* | test/* | ci/*
#   → open PR targeting main
#   → all jobs run as PR gates
#   → squash merge to main
#   → subset of jobs re-run on main to catch merge surprises
#
# Deploy is handled by build-push-deploy.yml on push to main.

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:

  # ── Change detection (always runs first) ─────────────────────────────────
  # Diff base:
  #   PR    → merge-base with the PR base branch
  #   push  → last-build/main tag (set by build-push-deploy after a green build)
  # Falls back to "rebuild all" when the base is missing or unreachable.
  detect-changes:
    runs-on: docker
    container: alpine:3.20
    outputs:
      admin: ${{ steps.diff.outputs.admin }}
      backend: ${{ steps.diff.outputs.backend }}
      sdk: ${{ steps.diff.outputs.sdk }}
      portal: ${{ steps.diff.outputs.portal }}
      tts: ${{ steps.diff.outputs.tts }}
      crawler: ${{ steps.diff.outputs.crawler }}
      dsms_gateway: ${{ steps.diff.outputs.dsms_gateway }}
      dsms_node: ${{ steps.diff.outputs.dsms_node }}
      any_python: ${{ steps.diff.outputs.any_python }}
      any_node: ${{ steps.diff.outputs.any_node }}
      any: ${{ steps.diff.outputs.any }}
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git bash
          git clone --depth 200 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
          if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
            git fetch --depth 200 origin "${GITHUB_BASE_REF}" || true
          else
            git fetch --tags origin || true
          fi
      - name: Resolve base SHA
        run: |
          if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
            BASE=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD 2>/dev/null || true)
          else
            BASE=$(git rev-parse --verify refs/tags/last-build/main 2>/dev/null || true)
          fi
          echo "Base SHA: ${BASE:-<none>}"
          echo "BASE_SHA=${BASE}" >> "$GITHUB_ENV"
      - name: Detect changes
        id: diff
        run: bash scripts/detect-changes.sh

  # ── Branch naming convention (PR only) ──────────────────────────────────
  branch-name:
    runs-on: docker
    container: alpine:3.20
    if: github.event_name == 'pull_request'
    steps:
      - name: Validate branch name
        run: |
          BRANCH="${GITHUB_HEAD_REF}"
          if ! echo "$BRANCH" | grep -qE '^(feat|feature|fix|hotfix|chore|refactor|docs|test|ci)/.+'; then
            echo "::error::Branch '$BRANCH' does not follow naming convention."
            echo "Required prefix: feat/ feature/ fix/ hotfix/ chore/ refactor/ docs/ test/ ci/"
            exit 1
          fi
          echo "Branch name OK: $BRANCH"

  # ── Guardrail integrity (PR only) ────────────────────────────────────────
  guardrail-integrity:
    runs-on: docker
    container: alpine:3.20
    if: github.event_name == 'pull_request'
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git bash
          git clone --depth 20 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
          git fetch origin ${GITHUB_BASE_REF}:base
      - name: Require [guardrail-change] in commits touching guardrails
        run: |
          changed=$(git diff --name-only base...HEAD)
          echo "$changed" | grep -qE '^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$' || exit 0
          if ! git log base..HEAD --format=%B | grep -q '\[guardrail-change\]'; then
            echo "::error::Guardrail files modified without [guardrail-change] in any commit message."
            exit 1
          fi

  # ── LOC budget (only if files changed) ───────────────────────────────────
  loc-budget:
    runs-on: docker
    container: alpine:3.20
    needs: detect-changes
    if: needs.detect-changes.outputs.any == 'true'
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git bash
          git clone --depth 50 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Enforce 500-line hard cap
        run: |
          chmod +x scripts/check-loc.sh
          scripts/check-loc.sh

  # ── Secret scanning (PR only) ────────────────────────────────────────────
  secret-scan:
    runs-on: docker
    container: zricethezav/gitleaks:v8.21.2
    if: github.event_name == 'pull_request'
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git
          git clone --depth 50 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Scan for secrets
        run: |
          gitleaks detect --source . --no-git \
            --exit-code 1 \
            --redact \
            || { echo "::error::Secrets detected — remove them before merging."; exit 1; }

  # ── Go lint + build (PR only, gated on ai-compliance-sdk changes) ────────
  go-lint:
    runs-on: docker
    needs: detect-changes
    if: github.event_name == 'pull_request' && needs.detect-changes.outputs.sdk == 'true'
    container: golangci/golangci-lint:v1.64.8-alpine
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git
          # Full clone so `main` is a local ref — new-from-merge-base needs the merge base.
          git clone ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
          git checkout ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}
      - name: Lint ai-compliance-sdk
        run: |
          [ -d "ai-compliance-sdk" ] || exit 0
          cd ai-compliance-sdk
          golangci-lint run --timeout 5m ./...
      - name: Build ai-compliance-sdk
        run: |
          [ -d "ai-compliance-sdk" ] || exit 0
          cd ai-compliance-sdk
          go build ./...

  # ── Python lint + import check (PR only, gated on python service changes) ─
  python-lint:
    runs-on: docker
    needs: detect-changes
    if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any_python == 'true'
    container: python:3.12
    steps:
      - name: Checkout
        run: |
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Lint (ruff) + type-check (mypy)
        run: |
          pip install --quiet ruff mypy
          fail=0
          for svc in backend-compliance document-crawler dsms-gateway compliance-tts-service; do
            [ -d "$svc" ] || continue
            echo "=== ruff: $svc ===" && ruff check "$svc/" --output-format=github || fail=1
          done
          if [ -f "backend-compliance/mypy.ini" ]; then
            cd backend-compliance && mypy compliance/ || fail=1
          fi
          exit $fail
      - name: Import sanity check (catches NameError at collection time)
        run: |
          cd backend-compliance
          pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true
          export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
          python -c "import compliance; print('Import OK')" \
            || { echo "::error::compliance package fails to import — missing import or syntax error."; exit 1; }

  # ── Node.js lint + type-check (PR only, gated on Next.js service changes) ─
  nodejs-lint:
    runs-on: docker
    needs: detect-changes
    if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any_node == 'true'
    container: node:20-alpine
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Lint + type-check
        run: |
          fail=0
          for svc in admin-compliance developer-portal; do
            [ -d "$svc" ] || continue
            echo "=== $svc: install ===" && (cd "$svc" && npm ci --silent 2>/dev/null || npm install --silent)
            echo "=== $svc: next lint ===" && (cd "$svc" && npx next lint) || fail=1
            echo "=== $svc: tsc ===" && (cd "$svc" && npx tsc --noEmit) || fail=1
          done
          exit $fail

  # ── Node.js build — next build (gated on Next.js service changes) ───────
  nodejs-build:
    runs-on: docker
    container: node:20-alpine
    needs: detect-changes
    if: needs.detect-changes.outputs.any_node == 'true'
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Build Next.js services
        run: |
          fail=0
          for svc in admin-compliance developer-portal; do
            [ -d "$svc" ] || continue
            echo "=== $svc: install ==="
            (cd "$svc" && npm ci --silent 2>/dev/null || npm install --silent)
            echo "=== $svc: next build ==="
            (cd "$svc" && \
              NEXT_PUBLIC_API_URL=https://api-dev.breakpilot.ai \
              NEXT_PUBLIC_SDK_URL=https://sdk-dev.breakpilot.ai \
              npm run build) || fail=1
          done
          exit $fail

  # ── Dependency audit (PR only) ───────────────────────────────────────────
  dep-audit:
    runs-on: docker
    if: github.event_name == 'pull_request'
    container: python:3.12
    steps:
      - name: Checkout
        run: |
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Install Node.js + Go
        run: |
          curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1
          apt-get install -y nodejs golang-go > /dev/null 2>&1
      - name: Python — pip-audit
        run: |
          pip install --quiet pip-audit
          fail=0
          for svc in backend-compliance document-crawler dsms-gateway compliance-tts-service; do
            [ -f "$svc/requirements.txt" ] || continue
            echo "=== pip-audit: $svc ==="
            pip-audit -r "$svc/requirements.txt" --skip-editable -f columns || fail=1
          done
          exit $fail
      - name: Node.js — npm audit
        run: |
          fail=0
          for svc in admin-compliance developer-portal; do
            [ -d "$svc" ] || continue
            echo "=== npm audit: $svc ==="
            (cd "$svc" && npm audit --audit-level=moderate --json 2>/dev/null | \
              node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); \
                const hi=Object.values(d.vulnerabilities||{}).filter(v=>['high','critical'].includes(v.severity)).length; \
                if(hi>0){console.error('HIGH/CRITICAL: '+hi);process.exit(1)}") || fail=1
          done
          exit $fail
      - name: Go — govulncheck
        run: |
          [ -d "ai-compliance-sdk" ] || exit 0
          go install golang.org/x/vuln/cmd/govulncheck@latest 2>/dev/null
          cd ai-compliance-sdk && govulncheck ./... || true
          # Non-blocking until Go module versions are pinned

  # ── SBOM + vulnerability scan (PR only) ─────────────────────────────────
  sbom-scan:
    runs-on: docker
    if: github.event_name == 'pull_request'
    container: alpine:3.20
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git curl bash
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Install syft + grype
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh  | sh -s -- -b /usr/local/bin
          curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
      - name: Generate SBOM
        run: mkdir -p sbom-out && syft dir:. -o cyclonedx-json=sbom-out/sbom.cdx.json -q
      - name: Vulnerability scan (fail on high+)
        run: grype sbom:sbom-out/sbom.cdx.json --fail-on high -q

  # ── Tests (gated per service) ────────────────────────────────────────────
  test-go:
    runs-on: docker
    container: golang:1.24-alpine
    needs: detect-changes
    if: needs.detect-changes.outputs.sdk == 'true'
    env:
      CGO_ENABLED: "0"
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Test ai-compliance-sdk
        run: |
          [ -d "ai-compliance-sdk" ] || exit 0
          cd ai-compliance-sdk
          go test -v -coverprofile=coverage.out ./...
          go tool cover -func=coverage.out | tail -1

  iace-gt-coverage:
    runs-on: docker
    container: python:3.12
    needs: detect-changes
    if: needs.detect-changes.outputs.sdk == 'true'
    env:
      # Lower bound on Strong+Weak GT-Bremse coverage. Raise this number when
      # coverage improves; never lower it without an explicit decision.
      MIN_COVERAGE_PCT: "70"
    steps:
      - name: Checkout
        run: |
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: GT-Bremse measure-coverage report
        run: |
          python3 scripts/gt_measure_gap_analysis.py --json /tmp/gt_gap_report.json > /tmp/gt_gap_report.md
          echo "--- summary ---"
          head -8 /tmp/gt_gap_report.md
      - name: Enforce coverage threshold
        run: |
          python3 - <<'PY'
          import json, os, sys
          d = json.load(open('/tmp/gt_gap_report.json'))
          total = d['total']
          covered = d['ok_count'] + d['weak_count']
          pct = covered * 100 / total if total else 0.0
          threshold = float(os.environ['MIN_COVERAGE_PCT'])
          print(f"GT coverage (strong+weak): {covered}/{total} = {pct:.1f}% (threshold {threshold}%)")
          if pct < threshold:
              print(f"::error::GT-Bremse coverage regression — {pct:.1f}% < {threshold}%")
              sys.exit(1)
          PY

  test-python-backend:
    runs-on: docker
    container: python:3.12
    needs: detect-changes
    if: needs.detect-changes.outputs.backend == 'true'
    env:
      CI: "true"
    steps:
      - name: Checkout
        run: |
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Test backend-compliance
        run: |
          [ -d "backend-compliance" ] || exit 0
          cd backend-compliance
          export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
          pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true
          pip install --quiet --no-cache-dir pytest pytest-asyncio
          python -m pytest compliance/tests/ -v --tb=short

  test-python-document-crawler:
    runs-on: docker
    container: python:3.12
    needs: detect-changes
    if: needs.detect-changes.outputs.crawler == 'true'
    env:
      CI: "true"
    steps:
      - name: Checkout
        run: |
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Test document-crawler
        run: |
          [ -d "document-crawler" ] || exit 0
          cd document-crawler
          export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
          pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true
          pip install --quiet --no-cache-dir pytest pytest-asyncio
          python -m pytest tests/ -v --tb=short

  test-python-dsms-gateway:
    runs-on: docker
    container: python:3.12
    needs: detect-changes
    if: needs.detect-changes.outputs.dsms_gateway == 'true'
    env:
      CI: "true"
    steps:
      - name: Checkout
        run: |
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Test dsms-gateway
        run: |
          [ -d "dsms-gateway" ] || exit 0
          cd dsms-gateway
          export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
          pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true
          pip install --quiet --no-cache-dir pytest pytest-asyncio
          python -m pytest test_main.py -v --tb=short

  # ── P83: BUILD_SHA integrity (always) ────────────────────────────────────
  # Every Dockerfile must declare ARG BUILD_SHA + ENV BUILD_SHA so the
  # check-rebuild-needed.sh script can detect "old code in container" drift.
  # Every docker-compose build: block must pass BUILD_SHA through as a build
  # arg — otherwise the ARG defaults to "unknown" and the check is toothless.
  build-sha-integrity:
    runs-on: docker
    container: alpine:3.20
    steps:
      - name: Checkout
        run: |
          apk add --no-cache git python3 py3-yaml
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Validate every Dockerfile + compose block declares BUILD_SHA
        run: |
          python3 - <<'PY'
          import re, sys, glob
          fails = []
          # 1. Each Dockerfile must have ARG BUILD_SHA + ENV BUILD_SHA=${BUILD_SHA}
          for df in sorted(glob.glob("*/Dockerfile")):
              # Skip nested non-canonical Dockerfiles (e.g. admin-compliance/ai-compliance-sdk/Dockerfile)
              if df.count("/") > 1: continue
              src = open(df).read()
              if "ARG BUILD_SHA" not in src:
                  fails.append(f"{df}: missing ARG BUILD_SHA")
              if "ENV BUILD_SHA" not in src:
                  fails.append(f"{df}: missing ENV BUILD_SHA")
          # 2. Every build: block in docker-compose.yml must pass BUILD_SHA
          import yaml
          compose = yaml.safe_load(open("docker-compose.yml"))
          for name, svc in (compose.get("services") or {}).items():
              build = svc.get("build")
              if not isinstance(build, dict):
                  continue  # skipping pre-built image refs
              args = (build.get("args") or {})
              if "BUILD_SHA" not in args:
                  fails.append(f"docker-compose.yml: service '{name}' build.args missing BUILD_SHA")
          if fails:
              print("::error::BUILD_SHA integrity check failed:")
              for f in fails: print(f"  - {f}")
              sys.exit(1)
          print(f"OK: BUILD_SHA wired in all Dockerfiles + compose build blocks.")
          PY

  # ── OpenAPI contract validation (always) ─────────────────────────────────
  validate-canonical-controls:
    runs-on: docker
    container: python:3.12
    steps:
      - name: Checkout
        run: |
          git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
      - name: Validate controls
        run: python scripts/validate-controls.py
