name: Desktop

on:
  push:
    tags: [ "v*" ]
  pull_request:
  workflow_dispatch:

jobs:
  # ── Build (PRs + releases) ────────────────────────────────────────────────
  desktop-build:
    name: desktop-build-${{ matrix.name }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-latest
            name: macos-arm64
          - os: macos-15-intel
            name: macos-intel
          - os: windows-latest
            name: windows
          - os: ubuntu-22.04
            name: linux
          - os: ubuntu-22.04-arm
            name: linux-arm64

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up JDK 21
        uses: actions/setup-java@v5
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5
        with:
          cache-read-only: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}

      - name: Build desktop application (Linux/macOS)
        if: runner.os != 'Windows'
        run: ./gradlew --no-daemon -Dorg.gradle.jvmargs="-Xmx4g" desktop:build

      - name: Build desktop application (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: .\gradlew.bat --no-daemon "-Dorg.gradle.jvmargs=-Xmx4g" desktop:build


  # ── Package + notarize installers (release tags only) ────────────────────
  desktop-package:
    name: desktop-package-${{ matrix.name }}
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-latest
            name: macos-arm64
            task: customNotarizeMacApp
            artifact-pattern: "desktop/build/compose/notarized/Askimo-signed.dmg"
            artifact-suffix: "macos-arm64"
          - os: macos-15-intel
            name: macos-intel
            task: customNotarizeMacApp
            artifact-pattern: "desktop/build/compose/notarized/Askimo-signed.dmg"
            artifact-suffix: "macos-intel"
          - os: ubuntu-22.04
            name: linux
            task: packageDeb
            artifact-pattern: "desktop/build/compose/binaries/main/deb/*.deb"
            artifact-suffix: "linux"
          - os: windows-latest
            name: windows
            task: packageMsi
            artifact-pattern: "desktop/build/compose/binaries/main/msi/*.msi"
            artifact-suffix: "windows"
            arch: "x64"
          - os: ubuntu-22.04-arm
            name: linux-arm
            task: packageDeb
            artifact-pattern: "desktop/build/compose/binaries/main/deb/*.deb"
            artifact-suffix: "linux-arm64"
            arch: "arm64"

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up JDK 21
        uses: actions/setup-java@v5
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5
        with:
          cache-read-only: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}

      # Runs build + sign + notarize + staple in one Gradle task chain.
      # On macOS the chain is: build → signMacApp → notarizeApp → createSignedDmg → customNotarizeDmg → customNotarizeMacApp
      - name: Package and notarize installer (Linux/macOS)
        if: runner.os != 'Windows'
        env:
          MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: ./gradlew --no-daemon -Dorg.gradle.jvmargs="-Xmx4g" desktop:${{ matrix.task }}

      - name: Package installer (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: .\gradlew.bat --no-daemon "-Dorg.gradle.jvmargs=-Xmx4g" desktop:${{ matrix.task }}

      # ── CHECKPOINT 1: verify DMG is properly signed + stapled before upload ──
      # If this fails the whole pipeline stops — no point uploading a broken artifact.
      - name: "[macOS] Verify DMG signature and staple ticket before upload"
        if: runner.os == 'macOS'
        shell: bash
        run: |
          set -euo pipefail
          DMG="${{ matrix.artifact-pattern }}"

          echo "══════════════════════════════════════════════════════════════"
          echo "CHECKPOINT 1 — DMG before upload to artifact store"
          echo "══════════════════════════════════════════════════════════════"

          if [ ! -f "$DMG" ]; then
            echo "❌ DMG not found: $DMG"
            echo "Contents of desktop/build/compose/notarized/:"
            ls -la desktop/build/compose/notarized/ || true
            exit 1
          fi

          echo "── File ────────────────────────────────────────────────────"
          ls -lh "$DMG"
          shasum -a 256 "$DMG"

          echo "── Code signature ──────────────────────────────────────────"
          codesign --verify --verbose=2 "$DMG" 2>&1 || { echo "❌ codesign verify failed"; exit 1; }
          codesign --display --verbose=4 "$DMG" 2>&1 | grep -E "CDHash|TeamIdentifier|Identifier|Authority" || true

          echo "── Staple ticket ───────────────────────────────────────────"
          xcrun stapler validate "$DMG" 2>&1 || { echo "❌ STAPLE TICKET MISSING — Gradle task failed to staple"; exit 1; }
          echo "✅ Staple ticket present"

          echo "── Gatekeeper assessment ───────────────────────────────────"
          spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG" 2>&1 \
            || { echo "❌ Gatekeeper rejected DMG"; exit 1; }
          echo "✅ Gatekeeper accepts DMG"
          echo "══════════════════════════════════════════════════════════════"

      - name: Build uber JAR (Linux/macOS)
        if: runner.os != 'Windows'
        run: ./gradlew --no-daemon -Dorg.gradle.jvmargs="-Xmx4g" desktop:packageUberJarForCurrentOS

      - name: Build uber JAR (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: .\gradlew.bat --no-daemon "-Dorg.gradle.jvmargs=-Xmx4g" desktop:packageUberJarForCurrentOS

      - name: Locate artifacts
        id: locate
        shell: bash
        run: |
          set -euo pipefail
          PATTERN="${{ matrix.artifact-pattern }}"

          if [ "$RUNNER_OS" == "macOS" ]; then
            PKG="$PATTERN"
            if [ ! -f "$PKG" ]; then
              echo "❌ Installer not found at: $PKG" >&2
              ls -la desktop/build/compose/notarized/ || true
              exit 1
            fi
          else
            PKG="$(compgen -G "$PATTERN" 2>/dev/null | head -n1 || ls $PATTERN 2>/dev/null | head -n1 || true)"
            if [ -z "$PKG" ]; then
              echo "❌ No installer found matching: $PATTERN" >&2
              exit 1
            fi
          fi

          JAR=$(find desktop/build/compose/jars -name "*.jar" -type f 2>/dev/null | head -n1 || true)
          if [ -z "$JAR" ]; then
            echo "❌ No uber JAR found in desktop/build/compose/jars/" >&2
            exit 1
          fi

          echo "package=$PKG" >> $GITHUB_OUTPUT
          echo "jar=$JAR"     >> $GITHUB_OUTPUT
          echo "✓ Installer : $PKG"
          echo "✓ Uber JAR  : $JAR"

      - name: Upload installer artifact
        uses: actions/upload-artifact@v6
        with:
          name: askimo-desktop-installer-${{ matrix.artifact-suffix }}
          path: ${{ steps.locate.outputs.package }}
          if-no-files-found: error
          retention-days: 14

      - name: Upload uber JAR artifact
        uses: actions/upload-artifact@v6
        with:
          name: askimo-desktop-jar-${{ matrix.artifact-suffix }}
          path: ${{ steps.locate.outputs.jar }}
          if-no-files-found: error
          retention-days: 14

      - name: Upload notarization log on failure
        if: failure() && runner.os == 'macOS'
        uses: actions/upload-artifact@v6
        with:
          name: notarize-log-${{ matrix.name }}-attempt${{ github.run_attempt }}
          path: ~/Library/Logs/notarytool/
          if-no-files-found: warn
          retention-days: 7


  # ── Create GitHub release ─────────────────────────────────────────────────
  release:
    name: Create Desktop Release
    if: startsWith(github.ref, 'refs/tags/v')
    needs: [ desktop-package ]
    runs-on: macos-latest
    permissions:
      contents: write

    steps:
      - name: Download all installer artifacts
        uses: actions/download-artifact@v8
        with:
          path: dist

      # ── CHECKPOINT 2: confirm what arrived after upload-artifact round-trip ──
      # upload-artifact zips the file; that zip round-trip may strip the staple
      # ticket xattr. We re-staple from Apple CDN and HARD FAIL if it doesn't work.
      - name: "[macOS] Re-staple DMGs after artifact download"
        shell: bash
        run: |
          set -euo pipefail
          echo "══════════════════════════════════════════════════════════════"
          echo "CHECKPOINT 2 — DMGs after download-artifact"
          echo "══════════════════════════════════════════════════════════════"
          echo "Full artifact tree received:"
          ls -lhR dist/
          echo ""

          FAILED=0
          for platform in macos-arm64 macos-intel; do
            echo "──────────────────────────────────────────────────────────"
            echo "Platform: $platform"
            DMG=$(find "dist/askimo-desktop-installer-${platform}" -name "*.dmg" -type f 2>/dev/null | head -n1 || true)
            if [ -z "$DMG" ]; then
              echo "⚠️  No DMG found for $platform — skipping (matrix may have failed)"
              continue
            fi

            echo "   File    : $DMG"
            ls -lh "$DMG"
            echo "   SHA-256 : $(shasum -a 256 "$DMG")"

            echo "   Code signature:"
            codesign --verify --verbose=2 "$DMG" 2>&1 \
              && echo "   ✅ Signature OK" \
              || { echo "   ❌ SIGNATURE INVALID"; FAILED=1; continue; }
            codesign --display --verbose=4 "$DMG" 2>&1 | grep -E "CDHash|TeamIdentifier|Identifier|Authority|Signed Time" || true

            echo "   Staple ticket before re-staple:"
            xcrun stapler validate "$DMG" 2>&1 \
              && echo "   ℹ️  Ticket still present (survived zip)" \
              || echo "   ℹ️  Ticket stripped by upload-artifact zip (expected — will re-staple)"

            echo "   📎 Re-stapling from Apple CDN..."
            set +e
            xcrun stapler staple "$DMG" 2>&1
            STAPLE_EXIT=$?
            set -e
            if [ $STAPLE_EXIT -ne 0 ]; then
              echo "   ❌ RE-STAPLE FAILED (exit $STAPLE_EXIT) for $platform"
              echo "   This means Apple CDN does not have a ticket for this DMG."
              echo "   The notarization step may have succeeded but the DMG was"
              echo "   re-created/modified after stapling — the hashes won't match."
              FAILED=1
              continue
            fi

            echo "   Staple ticket after re-staple:"
            xcrun stapler validate "$DMG" 2>&1 \
              && echo "   ✅ Re-staple verified" \
              || { echo "   ❌ VALIDATE FAILED after re-staple"; FAILED=1; }

            echo "   SHA-256 after re-staple: $(shasum -a 256 "$DMG")"
            echo ""
          done

          echo "══════════════════════════════════════════════════════════════"
          if [ "$FAILED" -ne 0 ]; then
            echo "❌ CHECKPOINT 2 FAILED — one or more DMGs are not properly notarized."
            echo "   Do NOT release. Fix the notarization pipeline and re-run."
            exit 1
          fi
          echo "✅ CHECKPOINT 2 PASSED — all DMGs re-stapled successfully"

      - name: Prepare release assets
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p release

          echo "══════════════════════════════════════════════════════════════"
          echo "Prepare release assets"
          echo "══════════════════════════════════════════════════════════════"

          # ── macOS ARM64 DMG ──────────────────────────────────────────────
          if [ -d "dist/askimo-desktop-installer-macos-arm64" ]; then
            SRC=$(find dist/askimo-desktop-installer-macos-arm64 -name "*.dmg" -type f | head -n1)
            if [ -z "$SRC" ]; then
              echo "❌ ARM64 DMG not found in dist/askimo-desktop-installer-macos-arm64/" >&2; exit 1
            fi
            DEST="release/Askimo-Desktop-macos-arm64.dmg"
            # cp preserves the embedded staple ticket (it's in the DMG code signature,
            # not a filesystem xattr, so a plain copy is sufficient)
            cp "$SRC" "$DEST"
            echo "✓ Copied ARM64 DMG  : $DEST  ($(du -sh "$DEST" | cut -f1))"
            # Verify ticket survived the copy
            xcrun stapler validate "$DEST" 2>&1 \
              && echo "  ✅ Staple ticket intact after copy" \
              || { echo "  ❌ Staple ticket LOST after cp — aborting"; exit 1; }
          fi

          # ── macOS Intel DMG ──────────────────────────────────────────────
          if [ -d "dist/askimo-desktop-installer-macos-intel" ]; then
            SRC=$(find dist/askimo-desktop-installer-macos-intel -name "*.dmg" -type f | head -n1)
            if [ -z "$SRC" ]; then
              echo "❌ Intel DMG not found in dist/askimo-desktop-installer-macos-intel/" >&2; exit 1
            fi
            DEST="release/Askimo-Desktop-macos-x64.dmg"
            cp "$SRC" "$DEST"
            echo "✓ Copied Intel DMG  : $DEST  ($(du -sh "$DEST" | cut -f1))"
            xcrun stapler validate "$DEST" 2>&1 \
              && echo "  ✅ Staple ticket intact after copy" \
              || { echo "  ❌ Staple ticket LOST after cp — aborting"; exit 1; }
          fi

          # ── Windows MSI ──────────────────────────────────────────────────
          if [ -d "dist/askimo-desktop-installer-windows" ]; then
            MSI=$(find dist/askimo-desktop-installer-windows -name "*.msi" -type f | head -n1)
            if [ -n "$MSI" ]; then
              cp "$MSI" "release/Askimo-Desktop-windows-x64.msi"
              echo "✓ Copied Windows MSI: release/Askimo-Desktop-windows-x64.msi"
            fi
          fi

          # ── Linux x64 DEB ────────────────────────────────────────────────
          if [ -d "dist/askimo-desktop-installer-linux" ]; then
            DEB=$(find dist/askimo-desktop-installer-linux -name "*.deb" -type f | head -n1)
            if [ -n "$DEB" ]; then
              cp "$DEB" "release/Askimo-Desktop-linux-x64.deb"
              echo "✓ Copied Linux x64  : release/Askimo-Desktop-linux-x64.deb"
            fi
          fi

          # ── Linux ARM64 DEB ──────────────────────────────────────────────
          if [ -d "dist/askimo-desktop-installer-linux-arm64" ]; then
            DEB=$(find dist/askimo-desktop-installer-linux-arm64 -name "*.deb" -type f | head -n1)
            if [ -n "$DEB" ]; then
              cp "$DEB" "release/Askimo-Desktop-linux-arm64.deb"
              echo "✓ Copied Linux ARM64: release/Askimo-Desktop-linux-arm64.deb"
            fi
          fi

          # ── Uber JARs ────────────────────────────────────────────────────
          for entry in \
            "askimo-desktop-jar-macos-arm64:askimo-desktop-macos-arm64.jar" \
            "askimo-desktop-jar-macos-intel:askimo-desktop-macos-x64.jar" \
            "askimo-desktop-jar-windows:askimo-desktop-windows-x64.jar" \
            "askimo-desktop-jar-linux:askimo-desktop-linux-x64.jar" \
            "askimo-desktop-jar-linux-arm64:askimo-desktop-linux-arm64.jar"; do
            artifact="${entry%%:*}"
            destname="${entry##*:}"
            if [ -d "dist/$artifact" ]; then
              JAR=$(find "dist/$artifact" -name "*.jar" -type f | head -n1)
              if [ -n "$JAR" ]; then
                cp "$JAR" "release/$destname"
                echo "✓ Copied JAR        : release/$destname"
              fi
            fi
          done

          echo ""
          if [ -z "$(ls release/ 2>/dev/null)" ]; then
            echo "❌ No release files found in release/ — aborting" >&2
            exit 1
          fi

          echo "Computing checksums..."
          cd release && shasum -a 256 * > SHA256SUMS.txt && cd ..

          echo ""
          echo "Release files prepared:"
          ls -lh release/
          echo "══════════════════════════════════════════════════════════════"

      # ── CHECKPOINT 3: final gate before upload to GitHub Releases ────────
      # Hard fail if staple ticket or Gatekeeper check is missing.
      # This is the LAST line of defence before files reach users.
      - name: "[macOS] Final validation of release DMGs before GitHub upload"
        shell: bash
        run: |
          set -euo pipefail
          FAILED=0

          echo "══════════════════════════════════════════════════════════════"
          echo "CHECKPOINT 3 — Final gate before GitHub Releases upload"
          echo "══════════════════════════════════════════════════════════════"

          shopt -s nullglob
          DMGS=(release/Askimo-Desktop-macos-*.dmg)
          if [ ${#DMGS[@]} -eq 0 ]; then
            echo "⚠️  No macOS DMGs found in release/ — skipping macOS validation"
          fi

          for DMG in "${DMGS[@]}"; do
            echo "──────────────────────────────────────────────────────────"
            echo "Validating: $DMG"
            ls -lh "$DMG"
            echo "SHA-256: $(shasum -a 256 "$DMG")"

            echo "Code signature:"
            codesign --verify --verbose=2 "$DMG" 2>&1 \
              && echo "✅ Signature OK" \
              || { echo "❌ SIGNATURE INVALID — DO NOT RELEASE"; FAILED=1; }
            codesign --display --verbose=4 "$DMG" 2>&1 | grep -E "CDHash|TeamIdentifier|Identifier|Authority|Signed Time" || true

            echo "Staple ticket:"
            xcrun stapler validate "$DMG" 2>&1 \
              && echo "✅ Staple ticket present" \
              || { echo "❌ STAPLE TICKET MISSING — DO NOT RELEASE"; FAILED=1; }

            echo "Gatekeeper assessment:"
            spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG" 2>&1 \
              && echo "✅ Gatekeeper accepts" \
              || { echo "❌ Gatekeeper REJECTS — DO NOT RELEASE"; FAILED=1; }
            echo ""
          done

          echo "══════════════════════════════════════════════════════════════"
          if [ "$FAILED" -ne 0 ]; then
            echo "❌ CHECKPOINT 3 FAILED — one or more DMGs did not pass validation."
            echo "   Aborting release. Check the output above for details."
            exit 1
          fi
          echo "✅ CHECKPOINT 3 PASSED — all DMGs are safe to upload to GitHub Releases"

      - name: Upload to GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: |
            release/*
          # Only upload files — do NOT touch the release body/notes.
          # Notes are authored manually; contributors are appended by the contributors job.
          append_body: false
          fail_on_unmatched_files: true
          prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
          draft: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}



  contributors:
    name: Append contributors to release notes
    if: startsWith(github.ref, 'refs/tags/v')
    needs: [ release ]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout (full history for git log)
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Build contributors section and append to release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          TAG="${{ github.ref_name }}"

          # Fetch current release body first
          CURRENT_BODY=$(gh release view "$TAG" --json body --jq '.body' 2>/dev/null || echo "")

          # Skip if OUR contributors section already exists (idempotent re-runs).
          # GitHub's auto-generated section uses "<!-- Release notes generated"
          # as a marker; ours does not — so we key on our unique thank-you line.
          if echo "$CURRENT_BODY" | grep -qF "Thank you to everyone who contributed"; then
            echo "ℹ️  Contributors section already present — skipping to preserve manual edits."
            exit 0
          fi

          # Skip if release notes are empty — don't add contributors to a blank body.
          TRIMMED=$(echo "$CURRENT_BODY" | tr -d '[:space:]')
          if [ -z "$TRIMMED" ]; then
            echo "ℹ️  Release notes are empty — skipping contributors until notes are written."
            exit 0
          fi

          # Find the previous tag
          PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${TAG}$" | head -n1 || true)
          RANGE="${PREV_TAG:+${PREV_TAG}..}${TAG}"
          echo "Contributors range: $RANGE"

          # Collect unique commit SHAs (exclude bots and GitHub noreply addresses)
          mapfile -t COMMITS < <(
            git log "$RANGE" --format='%H|%aE' \
              | grep -viE '\[bot\]|noreply\.github\.com' \
              | sort -u
          )

          # Resolve each commit to a GitHub login + avatar via the Commits API.
          declare -A SEEN_LOGINS
          CONTRIBUTOR_LINES=()

          for ENTRY in "${COMMITS[@]}"; do
            SHA="${ENTRY%%|*}"
            JSON=$(gh api "/repos/${GITHUB_REPOSITORY}/commits/${SHA}" \
              --jq '{login: .author.login, avatar: .author.avatar_url, name: .commit.author.name}' \
              2>/dev/null || echo "{}")

            LOGIN=$(echo "$JSON" | jq -r '.login // empty')
            AVATAR=$(echo "$JSON" | jq -r '.avatar // empty')
            NAME=$(echo "$JSON"   | jq -r '.name   // empty')

            # Skip bots and unresolved entries
            if [ -z "$LOGIN" ] || echo "$LOGIN" | grep -qiE '\[bot\]|bot$'; then
              continue
            fi

            # Deduplicate
            [ -n "${SEEN_LOGINS[$LOGIN]+x}" ] && continue
            SEEN_LOGINS[$LOGIN]=1

            if [ -n "$AVATAR" ]; then
              LINE="- <img src=\"${AVATAR}&s=20\" width=\"20\" height=\"20\" style=\"border-radius:50%\"> [@${LOGIN}](https://github.com/${LOGIN})"
            else
              LINE="- [@${LOGIN}](https://github.com/${LOGIN})"
            fi
            [ -n "$NAME" ] && LINE="${LINE} — ${NAME}"
            CONTRIBUTOR_LINES+=("$LINE")
          done

          if [ ${#CONTRIBUTOR_LINES[@]} -eq 0 ]; then
            echo "No human contributors resolved — skipping append."
            exit 0
          fi

          echo "Found ${#CONTRIBUTOR_LINES[@]} contributor(s)."

          # Strip any existing GitHub-auto-generated "## Contributors" block from
          # CURRENT_BODY so we replace it with our richer version (avatar + link).
          # GitHub's block starts with "## Contributors" and ends at EOF or next "## ".
          STRIPPED_BODY=$(echo "$CURRENT_BODY" | awk '
            /^## Contributors/ { skip=1; next }
            skip && /^## /     { skip=0 }
            !skip              { print }
          ')

          # Build our contributors section
          SECTION=$'\n\n## Contributors\n\nThank you to everyone who contributed to this release! 🎉\n\n'
          for LINE in "${CONTRIBUTOR_LINES[@]}"; do
            SECTION+="${LINE}"$'\n'
          done

          NEW_BODY="${STRIPPED_BODY}${SECTION}"

          gh release edit "$TAG" --notes "$NEW_BODY"
          echo "✅ Contributors section appended to release $TAG"

