name: CI

on:
  push:
    branches: [main]
    paths:
      - '**.go'
      - 'go.mod'
      - 'go.sum'
      - 'Makefile'
      - 'scripts/**'
      - 'webchat/**'
      - 'internal/webchat/**'
      - 'configs/**'
      - 'docs/**/*.md'
      - 'cmd/build-docs/**'
      - '.github/workflows/**'
  pull_request:
    branches: [main]
    paths:
      - '**.go'
      - 'go.mod'
      - 'go.sum'
      - 'Makefile'
      - 'scripts/**'
      - 'webchat/**'
      - 'internal/webchat/**'
      - 'configs/**'
      - 'docs/**/*.md'
      - 'cmd/build-docs/**'
      - '.github/workflows/**'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read
  pull-requests: write

jobs:
  # ── Guard: block large files (大门) ─────────────────────────────────────
  large-file-guard:
    name: Large File Guard
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1

      - name: Scan for files >1MB
        run: |
          OVERSIZED=""
          while IFS= read -r blob_info; do
            size=$(echo "$blob_info" | awk '{print $2}')
            path=$(echo "$blob_info" | awk '{print $3}')
            case "$path" in
              assets/*|docs/assets/*|docs/channels/*/guides/*) continue ;;
            esac
            if [ "$size" -gt 1048576 ] 2>/dev/null; then
              OVERSIZED="$OVERSIZED
            $path ($(( size / 1048576 ))MB)"
            fi
          done < <(git ls-tree -r HEAD | git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' | awk '$1 == "blob" && $2 > 1048576')

          if [ -n "$OVERSIZED" ]; then
            echo "::error::Large files (>1MB) detected in HEAD tree:$OVERSIZED"
            exit 1
          fi
          echo "No oversized files in HEAD tree."

  # ── Test: race + coverage (parallel with Build) ────────────────────────
  # Skips webchat build entirely — tests don't import the embedded SPA.
  # Runs vet inline for fast failure before expensive compilation.
  test:
    needs: [large-file-guard]
    name: Test
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - uses: ./.github/actions/setup-go-webchat
        with:
          build-webchat: 'false'

      - name: Verify + Vet
        run: go mod verify && go vet ./...

      - name: Determine test scope
        id: scope
        env:
          EVENT_NAME: ${{ github.event_name }}
        run: |
          if [ "$EVENT_NAME" = "push" ]; then
            echo "pkgs=." >> "$GITHUB_OUTPUT"
            exit 0
          fi

          CHANGED=$(git diff --name-only origin/main... | grep '\.go$' || true)
          if [ -z "$CHANGED" ]; then
            echo "pkgs=." >> "$GITHUB_OUTPUT"
            exit 0
          fi

          AFFECTED=""
          for f in $CHANGED; do
            dir=$(dirname "$f")
            full_pkg="github.com/hrygo/hotplex/$dir"
            AFFECTED="$AFFECTED $full_pkg"
          done

          AFFECTED=$(echo "$AFFECTED" | tr ' ' '\n' | sort -u | grep -v '^$' | tr '\n' ' ')
          if [ -n "$AFFECTED" ]; then
            echo "pkgs=$AFFECTED" >> "$GITHUB_OUTPUT"
            echo "Testing affected packages: $AFFECTED"
          else
            echo "pkgs=." >> "$GITHUB_OUTPUT"
          fi

      - name: Run tests with race detector + coverage
        env:
          SCOPE: ${{ steps.scope.outputs.pkgs }}
        run: |
          ALL_PKGS=$(go list ./... | grep -v \
            -e 'internal/worker/proc' \
            -e 'internal/worker/pi' \
            -e 'cmd/hotplex' \
            -e 'e2e')

          if [ "$SCOPE" = "." ]; then
            TEST_PKGS="$ALL_PKGS"
          else
            TEST_PKGS=""
            for pkg in $ALL_PKGS; do
              for changed in $SCOPE; do
                if [ "$pkg" = "$changed" ]; then
                  TEST_PKGS="$TEST_PKGS $pkg"
                  break
                fi
                # Include packages that import the changed package
                if go list -f '{{range .Imports}}{{if eq . "'"$changed"'"}}{{.}} {{end}}{{end}}' "$pkg" 2>/dev/null | grep -q "'"$changed"'"; then
                  TEST_PKGS="$TEST_PKGS $pkg"
                  break
                fi
              done
            done
          fi

          if [ -z "$TEST_PKGS" ]; then
            echo "No test packages affected by this change"
            printf 'mode: atomic\n' > coverage.out
            exit 0
          fi

          # shellcheck disable=SC2086,SC2046
          go test -race -timeout=10m -coverprofile=coverage.out -covermode=atomic $TEST_PKGS

      - name: Display coverage
        if: always()
        run: go tool cover -func=coverage.out 2>/dev/null | tail -1 || true

      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: coverage
          path: coverage.out

  # ── Build verification + Lint (parallel with Test) ────────────────────
  # Builds with webchat (uses cached output when available).
  # Runs vet + build + lint — all static checks in one job.
  build:
    needs: [large-file-guard]
    name: Build
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6

      - uses: ./.github/actions/setup-go-webchat

      - name: Verify + Vet + Build
        run: go mod verify && go vet ./... && go build ./...

      - name: Build & validate docs
        run: make docs-build

      - name: Build gateway binary
        run: |
          mkdir -p bin
          go build -o bin/hotplex ./cmd/hotplex

      - name: Verify binary
        run: test -x ./bin/hotplex && ./bin/hotplex --help

      - name: Lint
        uses: golangci/golangci-lint-action@v9.2.0
        with:
          version: v2.11.4
          args: --timeout=10m

  # ── Coverage gate ───────────────────────────────────────────────────────
  coverage-check:
    needs: [test]
    if: always() && needs.test.result == 'success'
    name: Coverage Check
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-go@v6
        with:
          go-version: '1.26'
          cache: false

      - name: Download coverage
        uses: actions/download-artifact@v7
        with:
          name: coverage
          path: coverage-artifacts

      - name: Check per-package coverage thresholds
        run: |
          COV_FILE="coverage-artifacts/coverage.out"

          # Skip if coverage file is empty (no packages affected)
          if [ ! -s "$COV_FILE" ] || [ "$(wc -l < "$COV_FILE")" -le 1 ]; then
            echo "No coverage data (no affected packages). Skipping thresholds."
            exit 0
          fi

          # Per-package thresholds (longest-prefix match wins).
          declare -A THRESHOLDS
          THRESHOLDS["internal/security"]=80
          THRESHOLDS["pkg/events"]=45
          THRESHOLDS["pkg/aep"]=75
          THRESHOLDS["internal/gateway/testutil"]=0
          THRESHOLDS["internal/gateway"]=55
          THRESHOLDS["internal/session"]=70
          THRESHOLDS["internal/config"]=70
          THRESHOLDS["internal/worker/base"]=45
          THRESHOLDS["internal/worker"]=70
          THRESHOLDS["internal/worker/claudecode"]=53
          THRESHOLDS["internal/worker/opencodeserver"]=9
          THRESHOLDS["internal/worker/noop"]=80
          THRESHOLDS["internal/messaging/feishu"]=48
          THRESHOLDS["internal/worker/codexcli"]=51
          THRESHOLDS["internal/worker/pi"]=0
          THRESHOLDS["internal/messaging/slack"]=35
          THRESHOLDS["internal/messaging"]=20

          FAILED=0
          {
            echo "## Per-Package Coverage Report"
            echo "| Package | Coverage | Threshold | Status |"
            echo "|---------|----------|-----------|--------|"
          } >> "$GITHUB_STEP_SUMMARY"

          # Aggregate per-package coverage from raw profile (statement-weighted).
          declare -A PKG_TOTAL
          declare -A PKG_COVERED

          while IFS= read -r line; do
            [[ "$line" == mode:* ]] && continue
            FILE="${line%%:*}"
            PKG=$(dirname "$FILE")
            NUM_STMTS=$(echo "$line" | awk '{print $(NF-1)}')
            EXEC_COUNT=$(echo "$line" | awk '{print $NF}')
            PKG_TOTAL[$PKG]=$((${PKG_TOTAL[$PKG]:-0} + NUM_STMTS))
            if [ "$EXEC_COUNT" -gt 0 ] 2>/dev/null; then
              PKG_COVERED[$PKG]=$((${PKG_COVERED[$PKG]:-0} + NUM_STMTS))
            fi
          done < "$COV_FILE"

          for PKG in "${!PKG_TOTAL[@]}"; do
            TOTAL=${PKG_TOTAL[$PKG]}
            [ "$TOTAL" -eq 0 ] && continue
            COVERED=${PKG_COVERED[$PKG]:-0}
            COV=$(echo "scale=1; $COVERED * 100 / $TOTAL" | bc -l)

            # Find matching threshold (longest prefix match).
            MATCH=""
            for pattern in "${!THRESHOLDS[@]}"; do
              if [[ "$PKG" == *"$pattern"* ]]; then
                if [ -z "$MATCH" ] || [ ${#pattern} -gt ${#MATCH} ]; then
                  MATCH="$pattern"
                fi
              fi
            done

            if [ -n "$MATCH" ]; then
              THRESH=${THRESHOLDS[$MATCH]}
              PASS=$(echo "$COV >= $THRESH" | bc -l)
              if [ "$PASS" -ne 1 ]; then
                STATUS=":x: FAIL"
                FAILED=1
                echo "::error::Package $PKG coverage ${COV}% is below ${THRESH}% threshold."
              else
                STATUS=":white_check_mark: PASS"
              fi
              echo "| \`$PKG\` | ${COV}% | ${THRESH}% | $STATUS |" >> "$GITHUB_STEP_SUMMARY"
            fi
          done

          TOTAL_LINE=$(go tool cover -func="$COV_FILE" 2>/dev/null | tail -1 | grep -oE '[0-9]+\.[0-9]+')
          {
            echo ""
            echo "**Total coverage: ${TOTAL_LINE}%** (informational, not gated)"
          } >> "$GITHUB_STEP_SUMMARY"

          if [ "$FAILED" -ne 0 ]; then
            echo "::error::One or more packages are below their coverage thresholds."
            exit 1
          fi
          echo "All package coverage thresholds met."
