name: Build EXO macOS DMG

on:
  push:
    tags:
      - "v*"

jobs:
  build-macos-app:
    runs-on: "macos-26"
    env:
      SPARKLE_VERSION: 2.8.1
      SPARKLE_DOWNLOAD_PREFIX: ${{ secrets.SPARKLE_DOWNLOAD_PREFIX }}
      SPARKLE_FEED_URL: ${{ secrets.SPARKLE_FEED_URL }}
      SPARKLE_ED25519_PUBLIC: ${{ secrets.SPARKLE_ED25519_PUBLIC }}
      SPARKLE_ED25519_PRIVATE: ${{ secrets.SPARKLE_ED25519_PRIVATE }}
      SPARKLE_S3_BUCKET: ${{ secrets.SPARKLE_S3_BUCKET }}
      SPARKLE_S3_PREFIX: ${{ secrets.SPARKLE_S3_PREFIX }}
      AWS_REGION: ${{ secrets.AWS_REGION }}
      EXO_BUILD_NUMBER: ${{ github.run_number }}
      EXO_LIBP2P_NAMESPACE: ${{ github.ref_name }}

    steps:
      # ============================================================
      # Checkout and tag validation
      # ============================================================

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Derive release version from tag
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          if [[ "$VERSION" == *-alpha* ]]; then
            echo "IS_ALPHA=true" >> $GITHUB_ENV
          else
            echo "IS_ALPHA=false" >> $GITHUB_ENV
          fi
          echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV

      - name: Ensure tag commit is on main
        run: |
          git fetch origin main
          # Alpha tags can be on any branch, production tags must be on main
          if [[ "$IS_ALPHA" == "true" ]]; then
            echo "Alpha tag detected, skipping main branch check"
          elif ! git merge-base --is-ancestor origin/main HEAD; then
            echo "Production tag must point to a commit on main"
            exit 1
          fi

      # ============================================================
      # Install dependencies
      # ============================================================

      - name: Select Xcode 26.2
        run: |
          sudo xcode-select -s /Applications/Xcode_26.2.app
          if ! xcrun -f metal >/dev/null 2>&1; then
            echo "Metal toolchain is not installed."
            exit 1
          fi

      - name: Install Homebrew packages
        run: brew install just awscli macmon

      - name: Install UV
        uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true
          cache-dependency-glob: uv.lock

      - name: Setup Python
        run: |
          uv python install
          uv sync --locked

      - name: Build dashboard
        run: |
          cd dashboard
          npm ci
          npm run build

      - name: Install Sparkle CLI
        run: |
          CLI_URL="${SPARKLE_CLI_URL:-https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz}"
          echo "Downloading Sparkle CLI from: $CLI_URL"
          mkdir -p /tmp/sparkle
          curl --fail --location --output /tmp/sparkle.tar.xz "$CLI_URL"
          tar -xJf /tmp/sparkle.tar.xz -C /tmp/sparkle --strip-components=1
          echo "SPARKLE_BIN=/tmp/sparkle/bin" >> $GITHUB_ENV

      - name: Prepare code-signing keychain
        env:
          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
          PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
        run: |
          KEYCHAIN_PATH="$HOME/Library/Keychains/build.keychain-db"

          # Create fresh keychain
          security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"

          # Disable auto-lock (no timeout, no lock-on-sleep)
          security set-keychain-settings "$KEYCHAIN_PATH"

          # Add to search list while preserving existing keychains
          security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')

          # Set as default and unlock
          security default-keychain -s "$KEYCHAIN_PATH"
          security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"

          # Import certificate with full access for codesign
          echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/cert.p12
          security import /tmp/cert.p12 -k "$KEYCHAIN_PATH" -P "$MACOS_CERTIFICATE_PASSWORD" \
            -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
          rm /tmp/cert.p12

          # Allow codesign to access the key without prompting
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"

          # Verify keychain is unlocked and identity is available
          echo "Verifying signing identity..."
          security find-identity -v -p codesigning "$KEYCHAIN_PATH"

          # Setup provisioning profile
          mkdir -p "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
          echo "$PROVISIONING_PROFILE" | base64 --decode > "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/EXO.provisionprofile"

          # Export keychain path for other steps
          echo "BUILD_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV

      # ============================================================
      # Build the bundle
      # ============================================================

      - name: Build PyInstaller bundle
        run: uv run pyinstaller packaging/pyinstaller/exo.spec

      - name: Build Swift app
        env:
          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
          SPARKLE_FEED_URL: ${{ secrets.SPARKLE_FEED_URL }}
          SPARKLE_ED25519_PUBLIC: ${{ secrets.SPARKLE_ED25519_PUBLIC }}
        run: |
          cd app/EXO
          security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$BUILD_KEYCHAIN_PATH"
          SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$BUILD_KEYCHAIN_PATH" | awk -F '"' '{print $2}')
          xcodebuild clean build \
            -scheme EXO \
            -configuration Release \
            -derivedDataPath build \
            MARKETING_VERSION="$RELEASE_VERSION" \
            CURRENT_PROJECT_VERSION="$EXO_BUILD_NUMBER" \
            EXO_BUILD_TAG="$RELEASE_VERSION" \
            EXO_BUILD_COMMIT="$GITHUB_SHA" \
            SPARKLE_FEED_URL="$SPARKLE_FEED_URL" \
            SPARKLE_ED25519_PUBLIC="$SPARKLE_ED25519_PUBLIC" \
            CODE_SIGNING_IDENTITY="$SIGNING_IDENTITY" \
            CODE_SIGN_INJECT_BASE_ENTITLEMENTS=YES
          mkdir -p ../../output
          cp -R build/Build/Products/Release/EXO.app ../../output/EXO.app

      - name: Inject PyInstaller runtime
        run: |
          rm -rf output/EXO.app/Contents/Resources/exo
          mkdir -p output/EXO.app/Contents/Resources
          cp -R dist/exo output/EXO.app/Contents/Resources/exo

      - name: Codesign PyInstaller runtime
        env:
          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
        run: |
          cd output
          security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$BUILD_KEYCHAIN_PATH"
          SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$BUILD_KEYCHAIN_PATH" | awk -F '"' '{print $2}')
          RUNTIME_DIR="EXO.app/Contents/Resources/exo"
          find "$RUNTIME_DIR" -type f \( -perm -111 -o -name "*.dylib" -o -name "*.so" \) -print0 |
            while IFS= read -r -d '' file; do
              /usr/bin/codesign --force --timestamp --options runtime \
                --sign "$SIGNING_IDENTITY" "$file"
            done

      - name: Sign, notarize, and create DMG
        env:
          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
          APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
          APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
          APPLE_NOTARIZATION_TEAM: ${{ secrets.APPLE_NOTARIZATION_TEAM }}
        run: |
          cd output
          security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$BUILD_KEYCHAIN_PATH"
          SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$BUILD_KEYCHAIN_PATH" | awk -F '"' '{print $2}')
          /usr/bin/codesign --deep --force --timestamp --options runtime \
            --sign "$SIGNING_IDENTITY" EXO.app
          mkdir -p dmg-root
          cp -R EXO.app dmg-root/
          ln -s /Applications dmg-root/Applications
          DMG_NAME="EXO-${RELEASE_VERSION}.dmg"
          hdiutil create -volname "EXO" -srcfolder dmg-root -ov -format UDZO "$DMG_NAME"
          /usr/bin/codesign --force --timestamp --options runtime \
            --sign "$SIGNING_IDENTITY" "$DMG_NAME"
          if [[ -n "$APPLE_NOTARIZATION_USERNAME" ]]; then
            SUBMISSION_OUTPUT=$(xcrun notarytool submit "$DMG_NAME" \
              --apple-id "$APPLE_NOTARIZATION_USERNAME" \
              --password "$APPLE_NOTARIZATION_PASSWORD" \
              --team-id "$APPLE_NOTARIZATION_TEAM" \
              --wait --timeout 15m 2>&1)
            echo "$SUBMISSION_OUTPUT"

            SUBMISSION_ID=$(echo "$SUBMISSION_OUTPUT" | awk 'tolower($1)=="id:" && $2 ~ /^[0-9a-fA-F-]+$/ {print $2; exit}')
            STATUS=$(echo "$SUBMISSION_OUTPUT" | awk 'tolower($1)=="status:" {print $2; exit}')

            if [[ -n "$SUBMISSION_ID" ]]; then
              xcrun notarytool log "$SUBMISSION_ID" \
                --apple-id "$APPLE_NOTARIZATION_USERNAME" \
                --password "$APPLE_NOTARIZATION_PASSWORD" \
                --team-id "$APPLE_NOTARIZATION_TEAM" > notarization-log.txt || true
              echo "===== Notarization Log ====="
              cat notarization-log.txt
              echo "============================"
            fi

            if [[ "$STATUS" != "Accepted" ]]; then
              echo "Notarization failed with status: ${STATUS:-Unknown}"
              exit 1
            fi

            xcrun stapler staple "$DMG_NAME"
          fi

      - name: Generate Sparkle appcast
        env:
          SPARKLE_DOWNLOAD_PREFIX: ${{ env.SPARKLE_DOWNLOAD_PREFIX }}
          SPARKLE_ED25519_PRIVATE: ${{ secrets.SPARKLE_ED25519_PRIVATE }}
          IS_ALPHA: ${{ env.IS_ALPHA }}
        run: |
          set -euo pipefail
          cd output
          DOWNLOAD_PREFIX="${SPARKLE_DOWNLOAD_PREFIX:-https://assets.exolabs.net}"
          echo "$SPARKLE_ED25519_PRIVATE" > sparkle_ed25519.key
          chmod 600 sparkle_ed25519.key

          CHANNEL_FLAG=""
          if [[ "$IS_ALPHA" == "true" ]]; then
            CHANNEL_FLAG="--channel alpha"
            echo "Generating appcast for alpha channel"
          fi

          $SPARKLE_BIN/generate_appcast \
            --ed-key-file sparkle_ed25519.key \
            --download-url-prefix "$DOWNLOAD_PREFIX" \
            $CHANNEL_FLAG \
            .

      # ============================================================
      # Upload artifacts
      # ============================================================

      - name: Upload DMG
        uses: actions/upload-artifact@v4
        with:
          name: EXO-dmg-${{ env.RELEASE_VERSION }}
          path: output/EXO-${{ env.RELEASE_VERSION }}.dmg

      - name: Upload to S3
        if: env.SPARKLE_S3_BUCKET != ''
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ${{ env.AWS_REGION }}
          SPARKLE_S3_BUCKET: ${{ env.SPARKLE_S3_BUCKET }}
          SPARKLE_S3_PREFIX: ${{ env.SPARKLE_S3_PREFIX }}
          IS_ALPHA: ${{ env.IS_ALPHA }}
        run: |
          set -euo pipefail
          cd output
          PREFIX="${SPARKLE_S3_PREFIX:-}"
          if [[ -n "$PREFIX" && "${PREFIX: -1}" != "/" ]]; then
            PREFIX="${PREFIX}/"
          fi
          DMG_NAME="EXO-${RELEASE_VERSION}.dmg"
          aws s3 cp "$DMG_NAME" "s3://${SPARKLE_S3_BUCKET}/${PREFIX}${DMG_NAME}"
          if [[ "$IS_ALPHA" != "true" ]]; then
            aws s3 cp "$DMG_NAME" "s3://${SPARKLE_S3_BUCKET}/${PREFIX}EXO-latest.dmg"
          fi
          aws s3 cp appcast.xml "s3://${SPARKLE_S3_BUCKET}/${PREFIX}appcast.xml" --content-type application/xml --cache-control no-cache
