name: CodeQL - Analyze

on:
  pull_request:
    branches: [main, nightly, development]
  push:
    branches: [main]
  workflow_dispatch:
  schedule:
    - cron: '0 3 * * 1' # Mondays 03:00 UTC

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
  cancel-in-progress: true

env:
  GOTOOLCHAIN: auto

permissions:
  contents: read
  security-events: write
  actions: read
  pull-requests: read

jobs:
  analyze:
    name: CodeQL analysis (${{ matrix.language }})
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
      actions: read
      pull-requests: read
    strategy:
      fail-fast: false
      matrix:
        language: [ 'go', 'javascript-typescript' ]
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          ref: ${{ github.sha }}

      - name: Verify CodeQL parity guard
        if: matrix.language == 'go'
        run: bash scripts/ci/check-codeql-parity.sh

      - name: Initialize CodeQL
        uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
        with:
          languages: ${{ matrix.language }}
          queries: security-and-quality
          # Use CodeQL config to exclude documented false positives
          # Go: Excludes go/request-forgery for url_testing.go (has 4-layer SSRF defense)
          # See: .github/codeql/codeql-config.yml for full justification
          config-file: ./.github/codeql/codeql-config.yml

      - name: Setup Go
        if: matrix.language == 'go'
        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
        with:
          go-version: 1.26.0
          cache-dependency-path: backend/go.sum

      - name: Verify Go toolchain and build
        if: matrix.language == 'go'
        run: |
          set -euo pipefail
          cd backend
          go version
          MOD_GO_VERSION="$(awk '/^go / {print $2; exit}' go.mod)"
          ACTIVE_GO_VERSION="$(go env GOVERSION | sed 's/^go//')"

          case "$ACTIVE_GO_VERSION" in
            "$MOD_GO_VERSION"|"$MOD_GO_VERSION".*)
              ;;
            *)
              echo "::error::Go toolchain mismatch: go.mod requires ${MOD_GO_VERSION}, active is ${ACTIVE_GO_VERSION}"
              exit 1
              ;;
          esac

          go build ./...

      - name: Prepare SARIF output directory
        run: mkdir -p sarif-results

      - name: Autobuild
        uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
        with:
          category: "/language:${{ matrix.language }}"
          output: sarif-results/${{ matrix.language }}

      - name: Check CodeQL Results
        if: always()
        run: |
          set -euo pipefail
          SARIF_DIR="sarif-results/${{ matrix.language }}"

          if [ ! -d "$SARIF_DIR" ]; then
            echo "::error::Expected SARIF output directory is missing: $SARIF_DIR"
            echo "❌ **ERROR:** SARIF output directory is missing: $SARIF_DIR" >> "$GITHUB_STEP_SUMMARY"
            exit 1
          fi

          SARIF_FILE="$(find "$SARIF_DIR" -maxdepth 1 -type f -name '*.sarif' | head -n 1 || true)"

          {
            echo "## 🔒 CodeQL Security Analysis Results"
            echo ""
            echo "**Language:** ${{ matrix.language }}"
            echo "**Query Suite:** security-and-quality"
            echo ""
          } >> "$GITHUB_STEP_SUMMARY"

          if [ -z "$SARIF_FILE" ] || [ ! -r "$SARIF_FILE" ]; then
            echo "::error::Expected SARIF file is missing or unreadable: $SARIF_FILE"
            echo "❌ **ERROR:** SARIF file is missing or unreadable: $SARIF_FILE" >> "$GITHUB_STEP_SUMMARY"
            exit 1
          fi

          # shellcheck disable=SC2016
          EFFECTIVE_LEVELS_JQ='[
            .runs[] as $run
            | $run.results[]
            | . as $result
            | ($run.tool.driver.rules // []) as $rules
            | ((
                $result.level
                // (if (($result.ruleIndex | type) == "number") then ($rules[$result.ruleIndex].defaultConfiguration.level // empty) else empty end)
                // ([
                    $rules[]?
                    | select((.id // "") == ($result.ruleId // ""))
                    | (.defaultConfiguration.level // empty)
                  ][0] // empty)
                // ""
              ) | ascii_downcase)
          ]'

          echo "Found SARIF file: $SARIF_FILE"
          ERROR_COUNT=$(jq -r "${EFFECTIVE_LEVELS_JQ} | map(select(. == \"error\")) | length" "$SARIF_FILE")
          WARNING_COUNT=$(jq -r "${EFFECTIVE_LEVELS_JQ} | map(select(. == \"warning\")) | length" "$SARIF_FILE")
          NOTE_COUNT=$(jq -r "${EFFECTIVE_LEVELS_JQ} | map(select(. == \"note\")) | length" "$SARIF_FILE")

          {
            echo "**Findings:**"
            echo "- 🔴 Errors: $ERROR_COUNT"
            echo "- 🟡 Warnings: $WARNING_COUNT"
            echo "- 🔵 Notes: $NOTE_COUNT"
            echo ""

            if [ "$ERROR_COUNT" -gt 0 ]; then
              echo "❌ **BLOCKING:** CodeQL error-level security issues found"
              echo ""
              echo "### Top Issues:"
              echo '```'
              # shellcheck disable=SC2016
              jq -r '
                .runs[] as $run
                | $run.results[]
                | . as $result
                | ($run.tool.driver.rules // []) as $rules
                | ((
                    $result.level
                    // (if (($result.ruleIndex | type) == "number") then ($rules[$result.ruleIndex].defaultConfiguration.level // empty) else empty end)
                    // ([
                        $rules[]?
                        | select((.id // "") == ($result.ruleId // ""))
                        | (.defaultConfiguration.level // empty)
                      ][0] // empty)
                    // ""
                  ) | ascii_downcase) as $effectiveLevel
                | select($effectiveLevel == "error")
                | "\($effectiveLevel): \($result.ruleId // \"<unknown-rule>\"): \($result.message.text)"
              ' "$SARIF_FILE" | head -5
              echo '```'
            else
              echo "✅ No blocking CodeQL issues found"
            fi
          } >> "$GITHUB_STEP_SUMMARY"

          {
            echo ""
            echo "View full results in the [Security tab](https://github.com/${{ github.repository }}/security/code-scanning)"
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Fail on High-Severity Findings
        if: always()
        run: |
          set -euo pipefail
          SARIF_DIR="sarif-results/${{ matrix.language }}"

          if [ ! -d "$SARIF_DIR" ]; then
            echo "::error::Expected SARIF output directory is missing: $SARIF_DIR"
            exit 1
          fi

          SARIF_FILE="$(find "$SARIF_DIR" -maxdepth 1 -type f -name '*.sarif' | head -n 1 || true)"

          if [ -z "$SARIF_FILE" ] || [ ! -r "$SARIF_FILE" ]; then
            echo "::error::Expected SARIF file is missing or unreadable: $SARIF_FILE"
            exit 1
          fi

          # shellcheck disable=SC2016
          ERROR_COUNT=$(jq -r '[
            .runs[] as $run
            | $run.results[]
            | . as $result
            | ($run.tool.driver.rules // []) as $rules
            | ((
                $result.level
                // (if (($result.ruleIndex | type) == "number") then ($rules[$result.ruleIndex].defaultConfiguration.level // empty) else empty end)
                // ([
                    $rules[]?
                    | select((.id // "") == ($result.ruleId // ""))
                    | (.defaultConfiguration.level // empty)
                  ][0] // empty)
                // ""
              ) | ascii_downcase) as $effectiveLevel
            | select($effectiveLevel == "error")
          ] | length' "$SARIF_FILE")

          if [ "$ERROR_COUNT" -gt 0 ]; then
            echo "::error::CodeQL found $ERROR_COUNT blocking findings (effective-level=error). Fix before merging. Policy: .github/security-severity-policy.yml"
            exit 1
          fi
