name: Build Native Binaries

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:
    inputs:
      publish:
        description: "Publish platform packages to npm"
        required: false
        default: "false"
        type: choice
        options:
          - "false"
          - "true"

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-14
            target: aarch64-apple-darwin
            platform: darwin-arm64
          - os: macos-14
            target: x86_64-apple-darwin
            platform: darwin-x64
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            platform: linux-x64-gnu
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            platform: linux-arm64-gnu
            cross: true
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            platform: win32-x64-msvc

    runs-on: ${{ matrix.os }}
    name: Build ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v6

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

      - name: Add Rust compilation target
        run: rustup target add ${{ matrix.target }}

      - name: Cache Rust build artifacts
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: native-${{ matrix.platform }}
          workspaces: |
            native -> target

      - name: Install cross-compilation tools (Linux ARM64)
        if: matrix.cross
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

      - name: Build native addon
        working-directory: native/crates/engine
        env:
          # CARGO_ENCODED_RUSTFLAGS overrides target-specific rustflags in
          # .cargo/config.toml, which sets -C target-cpu=native for dev builds.
          # CI must produce portable binaries.
          CARGO_ENCODED_RUSTFLAGS: ""
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.cross && 'aarch64-linux-gnu-gcc' || '' }}
        run: cargo build --release --target ${{ matrix.target }}

      - name: Prepare artifact (Unix)
        if: runner.os != 'Windows'
        run: |
          mkdir -p artifacts
          cp native/target/${{ matrix.target }}/release/libforge_engine.dylib artifacts/forge_engine.node 2>/dev/null || \
          cp native/target/${{ matrix.target }}/release/libforge_engine.so artifacts/forge_engine.node 2>/dev/null || \
          { echo "::error::No library found for ${{ matrix.platform }}"; exit 1; }
          ls -la artifacts/

      - name: Prepare artifact (Windows)
        if: runner.os == 'Windows'
        run: |
          mkdir artifacts
          copy native\target\${{ matrix.target }}\release\forge_engine.dll artifacts\forge_engine.node

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: native-${{ matrix.platform }}
          path: artifacts/forge_engine.node
          if-no-files-found: error

  publish:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true'
    runs-on: blacksmith-4vcpu-ubuntu-2404
    name: Publish platform packages

    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v6
        with:
          node-version: "24"
          registry-url: "https://registry.npmjs.org"
          cache: "npm"

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Copy binaries to platform packages
        run: |
          for platform in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu win32-x64-msvc; do
            cp "artifacts/native-${platform}/forge_engine.node" "native/npm/${platform}/forge_engine.node"
            echo "Copied binary for ${platform}"
            ls -la "native/npm/${platform}/"
          done

      - name: Sync platform package versions
        run: node native/scripts/sync-platform-versions.cjs

      - name: Detect prerelease version
        id: version-check
        run: |
          VERSION=$(node -p "require('./package.json').version")
          if echo "$VERSION" | grep -q '-next\.'; then
            echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
            echo "tag_flag=--tag next" >> "$GITHUB_OUTPUT"
            echo "Prerelease detected: ${VERSION} → publishing with --tag next"
          else
            echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
            echo "tag_flag=" >> "$GITHUB_OUTPUT"
            echo "Stable release: ${VERSION} → publishing with --tag latest (default)"
          fi

      - name: Publish platform packages
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          for platform in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu win32-x64-msvc; do
            echo "Publishing @sf-build/engine-${platform}..."
            cd "native/npm/${platform}"
            OUTPUT=$(npm publish --access public ${{ steps.version-check.outputs.tag_flag }} 2>&1) && echo "$OUTPUT" || {
              if echo "$OUTPUT" | grep -q "cannot publish over the previously published"; then
                echo "Already published, skipping"
              else
                echo "::error::Failed to publish ${platform}: $OUTPUT"
                exit 1
              fi
            }
            cd "$GITHUB_WORKSPACE"
          done

      - name: Verify platform packages are published
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "Verifying platform packages at version ${VERSION}..."
          # Exponential backoff: 5s, 10s, 20s, 30s, 30s (max 5 attempts, ~95s worst case vs fixed 30s + single check)
          DELAY=5
          for attempt in $(seq 1 5); do
            FAILED=0
            for platform in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu win32-x64-msvc; do
              PKG="@sf-build/engine-${platform}"
              PUBLISHED=$(npm view "${PKG}@${VERSION}" version 2>/dev/null || echo "")
              if [ "${PUBLISHED}" != "${VERSION}" ]; then
                FAILED=1
                break
              fi
            done
            if [ "${FAILED}" = "0" ]; then
              echo "All platform packages verified (attempt ${attempt})."
              break
            fi
            if [ "$attempt" = "5" ]; then
              echo "::error::One or more platform packages not found after 5 attempts. Aborting."
              for platform in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu win32-x64-msvc; do
                PKG="@sf-build/engine-${platform}"
                PUBLISHED=$(npm view "${PKG}@${VERSION}" version 2>/dev/null || echo "")
                if [ "${PUBLISHED}" = "${VERSION}" ]; then
                  echo "  ✓ ${PKG}@${VERSION}"
                else
                  echo "  ✗ ${PKG}@${VERSION} (got: '${PUBLISHED}')"
                fi
              done
              exit 1
            fi
            echo "  Attempt ${attempt}: not all packages visible yet, retrying in ${DELAY}s..."
            sleep "$DELAY"
            DELAY=$((DELAY * 2))
            if [ "$DELAY" -gt 30 ]; then DELAY=30; fi
          done

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Verify dist exists
        run: test -s dist/loader.js || { echo "::error::dist/loader.js missing or empty after build"; exit 1; }

      - name: Validate package is installable
        run: npm run validate-pack

      - name: Publish main package
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          # --ignore-scripts: skip prepublishOnly since we built explicitly above
          OUTPUT=$(npm publish --ignore-scripts ${{ steps.version-check.outputs.tag_flag }} 2>&1) && echo "$OUTPUT" || {
            if echo "$OUTPUT" | grep -q "cannot publish over the previously published\|You cannot publish over"; then
              echo "Already published, skipping"
            else
              echo "$OUTPUT"
              exit 1
            fi
          }

      - name: Post-publish smoke test
        run: |
          VERSION=$(node -p "require('./package.json').version")
          TMPDIR=$(mktemp -d)
          cd "$TMPDIR"
          npm init -y > /dev/null 2>&1

          # Wait for npm registry with exponential backoff (5s, 10s, 20s, 30s, 30s, 30s, 30s — max ~155s vs fixed 5min)
          echo "Waiting for sf-pi@${VERSION} to appear on npm..."
          DELAY=5
          for attempt in $(seq 1 8); do
            PUBLISHED=$(npm view "sf-pi@${VERSION}" version 2>/dev/null || echo "")
            if [ "${PUBLISHED}" = "${VERSION}" ]; then
              echo "  ✓ Version ${VERSION} visible on npm (attempt ${attempt})"
              break
            fi
            if [ "$attempt" = "8" ]; then
              echo "::warning::sf-pi@${VERSION} not visible on npm after 8 attempts — skipping smoke test"
              exit 0
            fi
            echo "  Attempt ${attempt}: not yet visible, retrying in ${DELAY}s..."
            sleep "$DELAY"
            DELAY=$((DELAY * 2))
            if [ "$DELAY" -gt 30 ]; then DELAY=30; fi
          done

          # Install and verify with backoff (5s, 10s, 20s)
          echo "Installing sf-pi@${VERSION}..."
          DELAY=5
          for attempt in 1 2 3; do
            if npm install "sf-pi@${VERSION}" 2>&1 | tee /tmp/install-output.txt; then
              echo "  ✓ Install succeeded"
              RAW=$(node node_modules/sf-pi/dist/loader.js --version 2>&1 || echo "FAILED")
              ACTUAL=$(echo "$RAW" | sed 's/\x1b\[[0-9;]*m//g' | grep -oE "^${VERSION}$" | head -1)
              if [ "$ACTUAL" = "$VERSION" ]; then
                echo "  ✓ sf --version = ${VERSION}"
                echo "Published package is functional"
                exit 0
              else
                echo "::error::Version mismatch: expected ${VERSION} in output:"
                echo "$RAW"
                exit 1
              fi
            fi
            echo "Install attempt ${attempt}/3 failed, retrying in ${DELAY}s..."
            cat /tmp/install-output.txt
            sleep "$DELAY"
            DELAY=$((DELAY * 2))
          done
          echo "::error::Smoke test failed — sf-pi@${VERSION} not installable"
          exit 1

      - name: Verify dist-tag after publish
        if: steps.version-check.outputs.is_prerelease == 'false'
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "Verifying npm dist-tag 'latest' points to ${VERSION}..."
          DELAY=5
          for attempt in $(seq 1 6); do
            LATEST=$(npm view sf-pi dist-tags.latest 2>/dev/null || echo "")
            if [ "${LATEST}" = "${VERSION}" ]; then
              echo "  ✓ npm dist-tags.latest = ${VERSION}"
              exit 0
            fi
            echo "  Attempt ${attempt}/6: latest=${LATEST}, expected=${VERSION}, retrying in ${DELAY}s..."
            sleep "$DELAY"
            DELAY=$((DELAY * 2))
            if [ "$DELAY" -gt 30 ]; then DELAY=30; fi
          done
          echo "::error::dist-tags.latest is '${LATEST}' but expected '${VERSION}' — run: npm dist-tag add sf-pi@${VERSION} latest"
          exit 1
