name: main

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  workflow_call:  # required to call this workflow from another workflow like release.yml

env:
  CARGO_TERM_COLOR: always
  FILE_ROOTSHELL: ../../rootshell/rootshell
  FILE_RAYHUNTER_DAEMON: ../../rayhunter-daemon/rayhunter-daemon
  RUSTFLAGS: "-Dwarnings"

jobs:
  files_changed:
    name: Detect file changes
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      code_changed: ${{ steps.files_changed.outputs.code_count != '0' }}
      daemon_changed: ${{ steps.files_changed.outputs.daemon_count != '0' }}
      daemon_needed: ${{ steps.files_changed.outputs.daemon_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
      web_changed: ${{ steps.files_changed.outputs.web_count != '0' }}
      docs_changed: ${{ steps.files_changed.outputs.docs_count != '0' }}
      installer_changed: ${{ steps.files_changed.outputs.installer_count != '0' }}
      installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count != '0' }}
      rootshell_needed: ${{ steps.files_changed.outputs.rootshell_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: detect file changes
        id: files_changed
        run: |
          lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }}

          # If we are on main, if workflow/cargo config files changed, or if
          # the latest commit message contains "#build-all", run everything.
          # Use #build-all in a commit message to force a full build on a PR
          # branch (useful for testing release builds without merging to main).
          if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo || git log -1 --format='%s %b' | grep -qF '#build-all'
          then
            echo "building everything"
            echo code_count=forced >> "$GITHUB_OUTPUT"
            echo daemon_count=forced >> "$GITHUB_OUTPUT"
            echo web_count=forced >> "$GITHUB_OUTPUT"
            echo docs_count=forced >> "$GITHUB_OUTPUT"
            echo installer_build=forced >> "$GITHUB_OUTPUT"
            echo installer_count=forced >> "$GITHUB_OUTPUT"
            echo installer_gui_count=forced >> "$GITHUB_OUTPUT"
            echo rootshell_count=forced >> "$GITHUB_OUTPUT"
          else
            echo "code_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon -e ^installer -e ^check -e ^lib -e ^rootshell -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
            echo "daemon_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon -e ^lib -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
            echo "web_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon/web | wc -l)" >> "$GITHUB_OUTPUT"
            echo "docs_count=$(git diff --name-only $lcommit...HEAD | grep -e ^book.toml -e ^doc | wc -l)" >> "$GITHUB_OUTPUT"
            echo "rootshell_count=$(git diff --name-only $lcommit...HEAD | grep -e ^rootshell | wc -l)" >> "$GITHUB_OUTPUT"

            installer_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer/ | wc -l)
            installer_gui_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer-gui | wc -l)

            if [ $installer_count != "0" ] || [ $installer_gui_count != "0" ]; then
              echo "installer_build=1" >> "$GITHUB_OUTPUT"
            else
              echo "installer_build=0" >> "$GITHUB_OUTPUT"
            fi

            echo "installer_count=$installer_count" >> "$GITHUB_OUTPUT"
            echo "installer_gui_count=$installer_gui_count" >> "$GITHUB_OUTPUT"
          fi

  mdbook_test:
    name: Test mdBook Documentation builds
    needs: files_changed
    if: needs.files_changed.outputs.docs_changed == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: Install mdBook
        run: |
          cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
      - name: Test mdBook
        run: mdbook test

  mdbook_publish:
    name: Publish mdBook to Github Pages
    needs: mdbook_test
    if: ${{ github.ref == 'refs/heads/main' }}
    permissions:
      pages: write
      contents: write
      id-token: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: Install mdBook
        run: |
          cargo install mdbook --no-default-features --features search --vers "^0.4" --locked

      - name: Build mdBook
        run: mdbook build

      - name: Setup Pages
        uses: actions/configure-pages@v4
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: book
      - name: Deploy to Github Pages
        uses: actions/deploy-pages@v4

  check_and_test:
    needs: files_changed
    if: needs.files_changed.outputs.code_changed == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
    - uses: actions/checkout@v4
      with:
        persist-credentials: false
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: rustfmt, clippy
    - uses: Swatinem/rust-cache@v2
    - name: Check formatting
      run: cargo fmt --all --check
    - name: Check
      run: |
        pushd daemon/web
        npm install
        npm run build
        popd
        NO_FIRMWARE_BIN=true cargo check --verbose
    - name: Run tests
      run: |
        NO_FIRMWARE_BIN=true cargo test --verbose
    - name: Run clippy
      run: |
        NO_FIRMWARE_BIN=true cargo clippy --verbose

  installer_gui_check:
    # we test the GUI installer separately to:
    # 1) mimic the default behavior of cargo commands for rayhunter devs where
    #    installer-gui isn't one of the default workspace packages
    # 2) avoid slowing down development on changes unrelated to the GUI installer
    needs: files_changed
    if: needs.files_changed.outputs.installer_gui_changed == 'true'
    # we run this on macos simply because no additional OS packages need to be
    # installed
    runs-on: macos-latest
    permissions:
      contents: read
    steps:
    - uses: actions/checkout@v4
      with:
        persist-credentials: false
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: clippy
    - uses: Swatinem/rust-cache@v2
    # we don't need to run cargo fmt here because both cargo fmt and cargo
    # fmt --all runs on all workspace packages so this is handled by
    # check_and_test above
    - name: Check
      run: NO_FIRMWARE_BIN=true cargo check --package installer-gui --verbose
    - name: Run clippy
      run: NO_FIRMWARE_BIN=true cargo clippy --package installer-gui --verbose

  test_daemon_frontend:
    needs: files_changed
    if: needs.files_changed.outputs.web_changed == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: daemon/web
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - run: npm install
      - run: npm run lint
      - run: npm run check
      - run: npm run test

  test_installer_frontend:
    needs: files_changed
    if: needs.files_changed.outputs.installer_gui_changed == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: installer-gui
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - run: npm install
      - run: npm run lint
      - run: npm run check

  windows_installer_check_and_test:
    needs: files_changed
    if: needs.files_changed.outputs.installer_changed == 'true'
    runs-on: windows-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: Swatinem/rust-cache@v2
      - name: cargo check
        shell: bash
        run: |
          cd installer
          NO_FIRMWARE_BIN=true cargo check --verbose
      - name: cargo test
        shell: bash
        run: |
          cd installer
          NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features

  build_rayhunter_check:
    if: needs.files_changed.outputs.daemon_changed == 'true'
    needs:
      - check_and_test
      - files_changed
    permissions:
      contents: read
      packages: write
    strategy:
      matrix:
        platform:
          - name: linux-x64
            os: ubuntu-latest
            target: x86_64-unknown-linux-musl
          - name: linux-armv7
            os: ubuntu-latest
            target: armv7-unknown-linux-musleabi
          - name: linux-aarch64
            os: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
          - name: macos-arm
            os: macos-latest
            target: aarch64-apple-darwin
          - name: macos-intel
            os: macos-latest
            target: x86_64-apple-darwin
          - name: windows-x86_64
            os: windows-latest
            target: x86_64-pc-windows-gnu
    runs-on: ${{ matrix.platform.os }}
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform.target }}
      - uses: Swatinem/rust-cache@v2
      - name: Build rayhunter-check
        run: cargo build --bin rayhunter-check --release --target ${{ matrix.platform.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: rayhunter-check-${{ matrix.platform.name }}
          path: target/${{ matrix.platform.target }}/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
          if-no-files-found: error

  build_rootshell:
    if: needs.files_changed.outputs.rootshell_needed == 'true'
    needs:
      - check_and_test
      - files_changed
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: armv7-unknown-linux-musleabihf
      - uses: Swatinem/rust-cache@v2
      - name: Build rootshell (armv7)
        run: cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
      - uses: actions/upload-artifact@v4
        with:
          name: rootshell
          path: target/armv7-unknown-linux-musleabihf/firmware/rootshell
          if-no-files-found: error

  build_rayhunter:
    if: needs.files_changed.outputs.daemon_needed == 'true'
    needs:
      - check_and_test
      - files_changed
    permissions:
      contents: read
      packages: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: armv7-unknown-linux-musleabihf
      - uses: Swatinem/rust-cache@v2
      - name: Install ARM cross-compilation toolchain
        run: sudo apt-get update && sudo apt-get install -y gcc-arm-linux-gnueabihf
      - name: Build rayhunter-daemon (armv7)
        run: |
          pushd daemon/web
          npm install
          npm run build
          popd
          # Run with -p so that cargo will select the minimum feature set for this package.
          #
          # Otherwise, it will consider the union of all requested features
          # from all packages in the workspace. For example, if installer
          # requires tokio with "full" feature, it will be included no matter
          # what the feature selection in rayhunter-daemon is.
          #
          # https://github.com/rust-lang/cargo/issues/4463
          CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
      - uses: actions/upload-artifact@v4
        with:
          name: rayhunter-daemon
          path: target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon
          if-no-files-found: error

  build_rust_installer:
    if: needs.files_changed.outputs.installer_changed == 'true'
    permissions:
      contents: read
      packages: write
    needs:
      - build_rayhunter
      - build_rootshell
      - files_changed
      - windows_installer_check_and_test
    strategy:
      matrix:
        platform:
          - name: linux-x64
            os: ubuntu-latest
            target: x86_64-unknown-linux-musl
          - name: linux-armv7
            os: ubuntu-latest
            target: armv7-unknown-linux-musleabi
          - name: linux-aarch64
            os: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
          - name: macos-arm
            os: macos-latest
            target: aarch64-apple-darwin
          - name: macos-intel
            os: macos-latest
            target: x86_64-apple-darwin
          - name: windows-x86_64
            os: windows-latest
            target: x86_64-pc-windows-gnu
    runs-on: ${{ matrix.platform.os }}
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/download-artifact@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform.target }}
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --package installer --bin installer --release --target ${{ matrix.platform.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: installer-${{ matrix.platform.name }}
          path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
          if-no-files-found: error

  build_installer_gui_linux:
    if: needs.files_changed.outputs.installer_gui_changed == 'true'
    permissions:
      contents: read
      packages: write
    needs:
      - build_rayhunter
      - build_rootshell
      - files_changed
      - installer_gui_check
      - test_installer_frontend
    strategy:
      matrix:
        platform:
          # we want to use the oldest supported version of ubuntu here to
          # maximize compatibility with older versions of glibc
          - name: linux-x64
            os: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
          - name: linux-aarch64
            os: ubuntu-22.04-arm
            target: aarch64-unknown-linux-gnu
    runs-on: ${{ matrix.platform.os }}
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/download-artifact@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform.target }}
      - uses: Swatinem/rust-cache@v2
      - name: Install tauri dependencies
        run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev xdg-utils
      - name: Build GUI installer
        shell: bash
        run: |
          cd installer-gui
          npm install
          npm run tauri build -- --target ${{ matrix.platform.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: gui-installer-${{ matrix.platform.name }}-appimage
          path: target/${{ matrix.platform.target }}/release/bundle/appimage/*.AppImage
          if-no-files-found: error
      - uses: actions/upload-artifact@v4
        with:
          name: gui-installer-${{ matrix.platform.name }}-deb
          path: target/${{ matrix.platform.target }}/release/bundle/deb/*.deb
          if-no-files-found: error
      - uses: actions/upload-artifact@v4
        with:
          name: gui-installer-${{ matrix.platform.name }}-rpm
          path: target/${{ matrix.platform.target }}/release/bundle/rpm/*.rpm
          if-no-files-found: error

  build_installer_gui_macos:
    if: needs.files_changed.outputs.installer_gui_changed == 'true'
    permissions:
      contents: read
      packages: write
    needs:
      - build_rayhunter
      - build_rootshell
      - files_changed
      - installer_gui_check
      - test_installer_frontend
    strategy:
      matrix:
        platform:
          - name: macos-arm
            target: aarch64-apple-darwin
          - name: macos-intel
            target: x86_64-apple-darwin
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/download-artifact@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform.target }}
      - uses: Swatinem/rust-cache@v2
      - name: Build GUI installer
        shell: bash
        run: |
          cd installer-gui
          npm install
          npm run tauri build -- --target ${{ matrix.platform.target }}
          cd ..
          mv "target/${{ matrix.platform.target }}/release/bundle/macos/"*.app .
          zip -r "rayhunter-installer-${{ matrix.platform.name }}.app.zip" ./*.app
      - uses: actions/upload-artifact@v4
        with:
          name: gui-installer-${{ matrix.platform.name }}-app
          path: ./*.app.zip
          if-no-files-found: error

  build_installer_gui_windows:
    if: needs.files_changed.outputs.installer_gui_changed == 'true'
    permissions:
      contents: read
      packages: write
    needs:
      - build_rayhunter
      - build_rootshell
      - files_changed
      - installer_gui_check
      - test_installer_frontend
    env:
      TARGET: x86_64-pc-windows-msvc
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/download-artifact@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ env.TARGET }}
      - uses: Swatinem/rust-cache@v2
      - name: Build GUI installer
        shell: bash
        run: |
          cd installer-gui
          npm install
          npm run tauri build -- --target ${{ env.TARGET }}
      - uses: actions/upload-artifact@v4
        with:
          name: gui-installer-msi
          path: target/${{ env.TARGET }}/release/bundle/msi/*.msi
          if-no-files-found: error
      - uses: actions/upload-artifact@v4
        with:
          name: gui-installer-exe
          path: target/${{ env.TARGET }}/release/bundle/nsis/*.exe
          if-no-files-found: error

  build_release_zip:
    permissions:
      contents: read
      packages: write
    needs:
      - build_rayhunter_check
      - build_rootshell
      - build_rayhunter
      - build_rust_installer
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform:
          - linux-x64
          - linux-aarch64
          - linux-armv7
          - macos-intel
          - macos-arm
          - windows-x86_64
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/download-artifact@v4
      - name: Fix executable permissions on binaries
        run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon
      - name: Get Rayhunter version
        id: get_version
        run: echo "VERSION=$(grep '^version' daemon/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
      - name: Setup versioned release directory
        run: |
          platform="${{ matrix.platform }}"
          dest="rayhunter-v${VERSION}-${{ matrix.platform }}"
          mkdir "$dest"
          # Handle installer with proper extension for Windows
          if [ "$platform" = "windows-x86_64" ]; then
            mv installer-$platform/installer.exe "$dest"/installer.exe
          else
            mv installer-$platform/installer "$dest"/installer
          fi
          cp -r rayhunter-check-* rayhunter-daemon dist/scripts "$dest"/
          zip -r "$dest.zip" "$dest"
          sha256sum "$dest.zip" > "$dest.zip.sha256"

      - name: Upload zip release and sha256
        uses: actions/upload-artifact@v4
        with:
          name: rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}
          path: |
            rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}.zip
            rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}.zip.sha256
          if-no-files-found: error
