name: Desktop Build & Release

on:
  push:
    tags:
      - 'v*'
      - 'desktop-v*'
  pull_request:
    branches: [main]
    paths:
      - 'frontend/**'
      - '.github/workflows/desktop.yml'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to build (e.g. v1.0.2.dev500). If set, autotag dispatches use this. github.ref still controls the checkout.'
        required: false
        type: string

concurrency:
  group: desktop-${{ inputs.tag || github.ref }}
  cancel-in-progress: true

permissions:
  contents: write

jobs:
  validate:
    runs-on: ubuntu-22.04

    steps:
      - uses: actions/checkout@v6

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libwebkit2gtk-4.1-dev \
            libgtk-3-dev \
            libappindicator3-dev \
            librsvg2-dev \
            patchelf \
            libxdo-dev \
            libdbus-1-dev

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '22'

      - name: Install frontend dependencies
        working-directory: frontend
        run: npm install

      - name: TypeScript type-check
        working-directory: frontend
        run: npx tsc --noEmit

      - name: Install Rust stable
        uses: dtolnay/rust-toolchain@stable

      - name: Rust cache
        uses: swatinem/rust-cache@v2
        with:
          workspaces: 'frontend/src-tauri -> target'

      - name: Create frontend dist stub
        run: mkdir -p frontend/dist && echo '<html><body></body></html>' > frontend/dist/index.html

      # `cargo test` builds the crate (same coverage as the old `cargo check`)
      # and runs the unit tests, including the #331 uv-sync error-formatting
      # helpers. Static command, no untrusted input — no injection surface.
      - name: Cargo test
        working-directory: frontend/src-tauri
        run: cargo test

  # Remove stale artifacts from the desktop-edge rolling pre-release so that
  # only the current build's files are available for download. (The stable
  # `desktop-latest` channel the installed app polls is never cleaned here.)
  clean-release:
    needs: [validate]
    if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
      - name: Delete old assets from desktop-edge
        if: "!startsWith(github.ref, 'refs/tags/')"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="desktop-edge"
          # List all asset IDs on the release and delete them
          ASSET_IDS=$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \
            --jq '.assets[].id' 2>/dev/null || true)
          for id in $ASSET_IDS; do
            echo "Deleting asset $id"
            gh api -X DELETE "repos/${{ github.repository }}/releases/assets/$id" || true
          done

  build-and-release:
    needs: [validate, clean-release]
    if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'

    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: ubuntu-22.04
            args: ''
          - platform: macos-14
            args: '--target universal-apple-darwin'
          - platform: windows-latest
            args: ''

    runs-on: ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v6
        with:
          # Full history + tags so the workflow_dispatch fallback in
          # "Determine release info" can derive the dev version from the
          # latest release tag (#526).
          fetch-depth: 0

      - name: Install system dependencies (Linux)
        if: matrix.platform == 'ubuntu-22.04'
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libwebkit2gtk-4.1-dev \
            libgtk-3-dev \
            libappindicator3-dev \
            librsvg2-dev \
            patchelf \
            libxdo-dev \
            libdbus-1-dev

      - name: Install Rust stable
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

      - name: Rust cache
        uses: swatinem/rust-cache@v2
        with:
          workspaces: 'frontend/src-tauri -> target'

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '22'

      - name: Install frontend dependencies
        working-directory: frontend
        run: npm install

      - name: Download Ollama sidecar
        shell: bash
        run: |
          cd frontend/src-tauri/scripts && chmod +x download-ollama.sh
          if [[ "${{ matrix.platform }}" == "macos-14" ]]; then
            ./download-ollama.sh aarch64-apple-darwin
            ./download-ollama.sh x86_64-apple-darwin
          else
            ./download-ollama.sh
          fi

      - name: Determine release info
        id: release-info
        shell: bash
        run: |
          if [[ "${{ github.ref }}" == refs/tags/desktop-v* ]]; then
            # Explicit stable desktop release tag
            VERSION="${{ github.ref_name }}"
            VERSION="${VERSION#desktop-v}"
            echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
            echo "name=Desktop ${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
            echo "prerelease=false" >> "$GITHUB_OUTPUT"
          elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
            # Auto-tagged rolling build from autotag.yml — use the same
            # version as the CLI/PyPI release so all surfaces stay in sync.
            # Rolling/dev builds go to the `desktop-edge` channel, NOT the
            # `desktop-latest` channel the installed app polls — so users on
            # stable are never auto-updated onto an unvetted dev build.
            VERSION="${{ github.ref_name }}"
            VERSION="${VERSION#v}"
            echo "tag=desktop-edge" >> "$GITHUB_OUTPUT"
            echo "name=Desktop (Edge Build)" >> "$GITHUB_OUTPUT"
            echo "prerelease=true" >> "$GITHUB_OUTPUT"
          else
            # workflow_dispatch fallback (manual UI dispatch without --ref).
            # Derive a PEP 440 dev version aligned with autotag.yml so we
            # don't burn the X.Y.Z release-version namespace.
            # pyproject.toml no longer carries a static version (#526), so the
            # base comes from the latest plain release tag (vX.Y.Z), matching
            # autotag.yml. .dev/.rc/desktop-* tags are excluded.
            LATEST_RELEASE=$(git tag --list 'v[0-9]*' --merged HEAD \
              | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
            if [[ -z "$LATEST_RELEASE" ]]; then
              echo "::error::No release tag (vX.Y.Z) reachable from HEAD"
              exit 1
            fi
            BASE="${LATEST_RELEASE#v}"
            MAJOR=$(echo "$BASE" | cut -d. -f1)
            MINOR=$(echo "$BASE" | cut -d. -f2)
            PATCH=$(echo "$BASE" | cut -d. -f3 | sed -E 's/[^0-9].*$//')
            NEXT_PATCH=$((PATCH + 1))
            BUILD=$(git rev-list --count HEAD)
            VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev${BUILD}"
            # Manual dispatches are also dev builds -> the edge channel.
            echo "tag=desktop-edge" >> "$GITHUB_OUTPUT"
            echo "name=Desktop (Edge Build)" >> "$GITHUB_OUTPUT"
            echo "prerelease=true" >> "$GITHUB_OUTPUT"
          fi
          echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
          # Tauri's build script requires strict SemVer (MAJOR.MINOR.PATCH[-pre][+build]).
          # PEP 440 dev releases (`1.0.2.dev661`) are NOT valid SemVer, so we
          # translate `.devN` to the SemVer-equivalent `-dev.N` prerelease form.
          # PyPI keeps the PEP 440 form; only the Tauri bundle uses SemVer.
          TAURI_VERSION="${VERSION/.dev/-dev.}"
          echo "tauri_version=${TAURI_VERSION}" >> "$GITHUB_OUTPUT"

      - name: Configure Apple signing
        if: runner.os == 'macOS'
        env:
          CERT: ${{ secrets.APPLE_CERTIFICATE }}
          CERT_PASS: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          SIGN_ID: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          A_ID: ${{ secrets.APPLE_ID }}
          A_PASS: ${{ secrets.APPLE_PASSWORD }}
          A_TEAM: ${{ secrets.APPLE_TEAM_ID }}
        shell: bash
        run: |
          if [ -n "$CERT" ]; then
            echo "APPLE_CERTIFICATE=$CERT" >> "$GITHUB_ENV"
            echo "APPLE_CERTIFICATE_PASSWORD=$CERT_PASS" >> "$GITHUB_ENV"
            echo "APPLE_SIGNING_IDENTITY=$SIGN_ID" >> "$GITHUB_ENV"
            echo "APPLE_ID=$A_ID" >> "$GITHUB_ENV"
            echo "APPLE_PASSWORD=$A_PASS" >> "$GITHUB_ENV"
            echo "APPLE_TEAM_ID=$A_TEAM" >> "$GITHUB_ENV"
            echo "Apple signing configured"
          else
            if [[ "${{ github.ref }}" == refs/tags/* ]]; then
              echo "::error::Apple signing secrets are required for release builds"
              exit 1
            fi
            echo "No Apple certificate configured, skipping code signing"
          fi

      - name: Build and release
        timeout-minutes: 120
        uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
          TAURI_CONFIG: '{"version":"${{ steps.release-info.outputs.tauri_version }}","bundle":{"externalBin":["binaries/ollama"]}}'
          # tauri-action runs beforeBuildCommand (npm run build:tauri -> vite
          # build), which requires this at build time (#587). Strict for
          # releases: a missing/empty secret fails the build by design.
          VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
        with:
          projectPath: frontend
          tauriScript: npx tauri
          tagName: ${{ steps.release-info.outputs.tag }}
          releaseName: ${{ steps.release-info.outputs.name }}
          releaseBody: 'Desktop application built from ${{ github.sha }}'
          releaseDraft: false
          prerelease: ${{ steps.release-info.outputs.prerelease }}
          includeUpdaterJson: true
          args: ${{ matrix.args }}

  # When a stable `desktop-v*` release is published, repoint the
  # `desktop-latest` auto-update channel (the endpoint the installed app
  # polls) at it. The stable release's own `latest.json` already references
  # this release's signed assets, so we copy it verbatim — installed apps are
  # only ever offered vetted stable builds, never `desktop-edge` dev builds.
  refresh-stable-channel:
    needs: [build-and-release]
    if: startsWith(github.ref, 'refs/tags/desktop-v')
    runs-on: ubuntu-latest
    steps:
      - name: Mirror stable latest.json into desktop-latest
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          STABLE_TAG: ${{ github.ref_name }}
          REPO: ${{ github.repository }}
        run: |
          set -euo pipefail
          # The stable release's updater manifest may take a moment to become
          # downloadable after tauri-action publishes it; retry briefly.
          URL="https://github.com/${REPO}/releases/download/${STABLE_TAG}/latest.json"
          for attempt in 1 2 3 4 5; do
            if curl -fsSL -o latest.json "$URL"; then
              echo "Fetched ${STABLE_TAG}/latest.json on attempt ${attempt}"
              break
            fi
            echo "latest.json not ready yet (attempt ${attempt}); sleeping 15s"
            sleep 15
          done
          test -s latest.json || { echo "::error::Could not fetch ${URL}"; exit 1; }
          # Ensure the channel release exists (prerelease so it never usurps
          # the stable "Latest" badge), then replace its manifest in place.
          if ! gh release view desktop-latest --repo "$REPO" >/dev/null 2>&1; then
            gh release create desktop-latest --repo "$REPO" \
              --prerelease \
              --title "Desktop Auto-Update Channel" \
              --notes "Auto-update channel pointer for the desktop app. Mirrors the latest stable \`desktop-v*\` release; the in-app updater polls this \`latest.json\`. Download the app from the latest stable release, not here."
          fi
          gh release upload desktop-latest latest.json --repo "$REPO" --clobber
          echo "desktop-latest now mirrors ${STABLE_TAG}"
