name: CI

on:
  pull_request:
    branches: [main]
    paths-ignore:
      - 'firmware/**'

concurrency:
  group: ci-${{ github.event.pull_request.number }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: 0

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          fetch-depth: 0
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: 1.93.0
          components: rustfmt, clippy
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        with:
          cache-bin: false

      - name: Ensure web/dist placeholder exists
        run: mkdir -p web/dist && touch web/dist/.gitkeep

      - name: Check formatting
        run: cargo fmt --all -- --check

      - name: Clippy
        run: cargo clippy --all-targets -- -D warnings

  bench-compile:
    name: Verify Benchmarks Compile
    runs-on: ubuntu-latest
    # 25 minutes — the bench compile takes 11-13 min on a warm
    # Swatinem/rust-cache hit and 14-16 min on a cold one (PRs that
    # add new source files invalidate the cache key).  15 minutes
    # was right at that boundary and intermittently timed out
    # (PRs #21, #26, #30 all hit the cold-cache tail).
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: 1.93.0
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        with:
          cache-bin: false

      - name: Ensure web/dist placeholder exists
        run: mkdir -p web/dist && touch web/dist/.gitkeep

      - name: Verify benchmarks compile
        run: cargo bench --no-run --locked

  lint-strict-delta:
    name: Strict Delta Lint
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          fetch-depth: 0
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: 1.93.0
          components: clippy
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        with:
          cache-bin: false

      - name: Ensure web/dist placeholder exists
        run: mkdir -p web/dist && touch web/dist/.gitkeep

      - name: Run strict delta lint gate
        run: bash scripts/ci/rust_strict_delta_gate.sh
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}

  test:
    name: Test
    runs-on: ubuntu-latest
    timeout-minutes: 60
    needs: [lint]
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: 1.93.0
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        with:
          cache-bin: false

      - name: Ensure web/dist placeholder exists
        run: mkdir -p web/dist && touch web/dist/.gitkeep

      - name: Install mold linker
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y mold

      - name: Install cargo-nextest
        run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin

      - name: Run tests
        run: cargo nextest run --locked
        env:
          CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: clang
          CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C link-arg=-fuse-ld=mold"

  build:
    name: Build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 40
    needs: [lint]
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: macos-14
            target: aarch64-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-msvc
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: 1.93.0
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        if: runner.os != 'Windows'
        with:
          cache-bin: false

      - name: Install mold linker
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y mold

      - name: Ensure web/dist placeholder exists
        shell: bash
        run: mkdir -p web/dist && touch web/dist/.gitkeep

      - name: Build release
        shell: bash
        run: cargo build --profile ci --locked --target ${{ matrix.target }}
        env:
          CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: clang
          CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C link-arg=-fuse-ld=mold"

  check-all-features:
    name: Check (all features)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    needs: [lint]
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          fetch-depth: 0
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: 1.93.0
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        with:
          cache-bin: false

      - name: Install system dependencies
        run: sudo apt-get update -qq && sudo apt-get install -y libudev-dev

      - name: Ensure web/dist placeholder exists
        run: mkdir -p web/dist && touch web/dist/.gitkeep

      - name: Check all features
        run: cargo check --features ci-all --locked

  feature-matrix:
    name: Feature Matrix (${{ matrix.feature }})
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs: [lint]
    strategy:
      fail-fast: false
      matrix:
        include:
          - feature: no-default
            flags: "--no-default-features"
          - feature: channel-nostr
            flags: "--no-default-features --features channel-nostr"
          - feature: channel-matrix
            flags: "--no-default-features --features channel-matrix"
          - feature: channel-lark
            flags: "--no-default-features --features channel-lark"
          - feature: whatsapp-web
            flags: "--no-default-features --features whatsapp-web"
          - feature: skill-creation
            flags: "--no-default-features --features skill-creation"
          - feature: observability-otel
            flags: "--no-default-features --features observability-otel"
          - feature: plugins-wasm
            flags: "--no-default-features --features plugins-wasm"
          - feature: rag-pdf
            flags: "--no-default-features --features rag-pdf"
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: 1.93.0
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        with:
          prefix-key: feature-matrix-${{ matrix.feature }}
          cache-bin: false

      - name: Ensure web/dist placeholder exists
        run: mkdir -p web/dist && touch web/dist/.gitkeep

      - name: Check ${{ matrix.feature }}
        run: cargo check ${{ matrix.flags }} --locked

  docs-quality:
    name: Docs Quality
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
        with:
          node-version-file: web/.nvmrc
      - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
        with:
          python-version: "3.12"

      - name: Run docs quality gate
        run: bash scripts/ci/docs_quality_gate.sh
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}

  version-drift:
    name: Version Drift Gate
    runs-on: ubuntu-latest
    timeout-minutes: 3
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - name: Check version alignment
        run: bash scripts/ci/check_version_drift.sh

  # Composite status check — branch protection requires this single job.
  gate:
    name: CI Required Gate
    if: always()
    needs: [lint, bench-compile, lint-strict-delta, test, build, docs-quality, check-all-features, feature-matrix, version-drift]
    runs-on: ubuntu-latest
    steps:
      - name: Check upstream job results
        env:
          HAS_FAILURE: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
        run: |
          if [[ "$HAS_FAILURE" == "true" ]]; then
            echo "::error::One or more upstream jobs failed or were cancelled"
            exit 1
          fi
