name: npm-rust-publish

on:
  workflow_call:
    inputs:
      version:
        required: true
        type: string

permissions:
  contents: read
  id-token: write
  checks: write

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

defaults:
  run:
    shell: bash

jobs:
  version:
    name: resolve version
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Read version
        id: version
        working-directory: rust
        run: |
          if [ -n "${{ inputs.version }}" ]; then
            version="${{ inputs.version }}"
          else
            version=$(node -p "require('./package.json').version")
          fi
          echo "version=${version}" >> "$GITHUB_OUTPUT"

  build:
    name: build ${{ matrix.target.key }}
    needs: version
    runs-on: ${{ matrix.target.runner }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      max-parallel: 6
      matrix:
        target:
          - key: linux-x64-gnu
            packageName: cpd-linux-x64-gnu
            os: linux
            cpu: x64
            libc: glibc
            rustTarget: x86_64-unknown-linux-gnu
            runner: ubuntu-22.04
          - key: linux-arm64-gnu
            packageName: cpd-linux-arm64-gnu
            os: linux
            cpu: arm64
            libc: glibc
            rustTarget: aarch64-unknown-linux-gnu
            runner: ubuntu-22.04-arm
          - key: linux-x64-musl
            packageName: cpd-linux-x64-musl
            os: linux
            cpu: x64
            libc: musl
            rustTarget: x86_64-unknown-linux-musl
            runner: ubuntu-latest
          - key: darwin-arm64
            packageName: cpd-darwin-arm64
            os: darwin
            cpu: arm64
            rustTarget: aarch64-apple-darwin
            runner: macos-latest
          - key: darwin-x64
            packageName: cpd-darwin-x64
            os: darwin
            cpu: x64
            rustTarget: x86_64-apple-darwin
            runner: macos-latest
          - key: windows-x64-msvc
            packageName: cpd-windows-x64-msvc
            os: win32
            cpu: x64
            rustTarget: x86_64-pc-windows-msvc
            runner: windows-latest

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

      - name: Check platform package already published (idempotency)
        id: platform-package
        run: |
          version="${{ needs.version.outputs.version }}"
          package_name="${{ matrix.target.packageName }}"
          if npm view "${package_name}@${version}" version >/dev/null 2>&1; then
            echo "${package_name}@${version} is already published; skipping build"
            echo "published=true" >> "$GITHUB_OUTPUT"
          else
            echo "published=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Install Rust toolchain
        if: steps.platform-package.outputs.published != 'true'
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: "1.93"
          targets: ${{ matrix.target.rustTarget }}

      - name: Cache Cargo registry and target
        if: steps.platform-package.outputs.published != 'true'
        uses: Swatinem/rust-cache@v2
        with:
          workspaces: "rust -> rust/target"
          cache-on-failure: true

      - name: Install cross-compilation tools (Linux ARM64)
        if: steps.platform-package.outputs.published != 'true' && matrix.target.key == 'linux-arm64-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV

      - name: Install musl tools
        if: steps.platform-package.outputs.published != 'true' && matrix.target.key == 'linux-x64-musl'
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools

      - name: Install cargo-nextest
        if: steps.platform-package.outputs.published != 'true'
        uses: taiki-e/install-action@nextest

      - name: Test
        # Skip for darwin-x64: cross-compiled from ARM Mac, tests cannot run on host
        if: steps.platform-package.outputs.published != 'true' && matrix.target.key != 'darwin-x64'
        working-directory: rust
        run: cargo nextest run --workspace --locked --target ${{ matrix.target.rustTarget }} --profile ci

      - name: Upload test results
        if: always() && steps.platform-package.outputs.published != 'true' && matrix.target.key != 'darwin-x64'
        uses: actions/upload-artifact@v4
        with:
          name: nextest-results-${{ matrix.target.key }}
          path: rust/target/nextest/ci/nextest.xml
          if-no-files-found: warn

      - name: Test Report
        if: always() && steps.platform-package.outputs.published != 'true' && matrix.target.key != 'darwin-x64'
        uses: dorny/test-reporter@v1
        with:
          name: Rust Tests (${{ matrix.target.key }})
          path: rust/target/nextest/ci/nextest.xml
          reporter: java-junit
          fail-on-error: false

      - name: Build
        if: steps.platform-package.outputs.published != 'true'
        working-directory: rust
        run: cargo build --release --locked --target ${{ matrix.target.rustTarget }} -p jscpd

      - name: Create platform package
        if: steps.platform-package.outputs.published != 'true'
        id: package
        working-directory: rust
        run: |
          package_dir="$(
            node scripts/npm-prebuilt-package.mjs \
              --target "${{ matrix.target.key }}" \
              --bin-dir "target/${{ matrix.target.rustTarget }}/release" \
              --out-dir "target/npm-prebuilt"
          )"
          echo "package_dir=${package_dir}" >> "$GITHUB_OUTPUT"

      - name: Smoke test (non-Windows)
        if: steps.platform-package.outputs.published != 'true' && matrix.target.os != 'win32'
        working-directory: rust
        run: |
          version="${{ needs.version.outputs.version }}"
          binary="./target/${{ matrix.target.rustTarget }}/release/cpd"
          BUILT_VERSION=$("$binary" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "NOT_FOUND")
          echo "Built: ${BUILT_VERSION}, Expected: ${version}"
          [ "$BUILT_VERSION" = "${version}" ] || { echo "Version mismatch!"; exit 1; }

      - name: Archive binary for release assets
        if: steps.platform-package.outputs.published != 'true'
        working-directory: rust
        run: |
          binary_name="cpd"
          if [ "${{ matrix.target.os }}" = "win32" ]; then
            binary_name="cpd.exe"
          fi
          mkdir -p target/release-assets
          tar -czf "target/release-assets/cpd-${{ matrix.target.key }}.tar.gz" \
            -C "target/${{ matrix.target.rustTarget }}/release" \
            "$binary_name"

      - name: Upload binary artifact
        if: steps.platform-package.outputs.published != 'true'
        uses: actions/upload-artifact@v4
        with:
          name: cpd-binary-${{ matrix.target.key }}
          path: rust/target/release-assets/cpd-${{ matrix.target.key }}.tar.gz
          retention-days: 1

      - name: Install pnpm
        if: steps.platform-package.outputs.published != 'true'
        uses: pnpm/action-setup@v4

      - name: Install Node
        if: steps.platform-package.outputs.published != 'true'
        uses: actions/setup-node@v5
        with:
          node-version: "22"
          registry-url: "https://registry.npmjs.org"
          package-manager-cache: false

      - name: Publish platform package
        if: steps.platform-package.outputs.published != 'true'
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        working-directory: rust
        run: |
          version="${{ needs.version.outputs.version }}"
          package_name="${{ matrix.target.packageName }}"
          package_dir="${{ steps.package.outputs.package_dir }}"
          if npm view "${package_name}@${version}" version >/dev/null 2>&1; then
            echo "${package_name}@${version} is already published; skipping"
            exit 0
          fi
          npm publish "$package_dir" --access public --provenance

  publish-main:
    name: publish main cpd package
    needs:
      - version
      - build
    if: ${{ always() && needs.build.result == 'success' }}
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Install Node
        uses: actions/setup-node@v5
        with:
          node-version: "22"
          registry-url: "https://registry.npmjs.org"
          package-manager-cache: false

      - name: Verify all platform packages are published
        working-directory: rust
        run: |
          version="${{ needs.version.outputs.version }}"
          targets=$(node -e "
            const t = require('./npm/prebuilt-targets.json');
            Object.values(t).forEach(v => console.log(v.packageName));
          ")
          missing=0
          for pkg in $targets; do
            if npm view "${pkg}@${version}" version >/dev/null 2>&1; then
              echo "${pkg}@${version} is published"
            else
              missing=$((missing + 1))
              echo "::error title=Missing platform package::${pkg}@${version} is not published"
            fi
          done
          if [ "$missing" -gt 0 ]; then
            echo "Missing ${missing} platform packages; cannot publish main package"
            exit 1
          fi

      - name: Publish main cpd package
        working-directory: rust
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          version="${{ needs.version.outputs.version }}"
          if npm view "cpd@${version}" version >/dev/null 2>&1; then
            echo "cpd@${version} is already published; skipping"
            exit 0
          fi
          npm publish --access public --provenance

  publish-jscpd:
    name: publish jscpd wrapper package
    needs:
      - version
      - publish-main
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Install cpd globally for fixture check
        working-directory: rust
        run: |
          version="${{ needs.version.outputs.version }}"
          # ponytail: npm registry CDN propagates with eventual consistency;
          # a freshly published cpd@${version} may 404 for seconds-to-minutes.
          # Poll npm view before installing. Switch to a registry webhook if
          # propagation latency ever exceeds ~5 minutes regularly.
          for i in $(seq 1 30); do
            if npm view "cpd@${version}" version >/dev/null 2>&1; then
              npm install -g "cpd@${version}"
              exit 0
            fi
            echo "Attempt $i/30: waiting for cpd@${version} to propagate on npm..."
            sleep 10
          done
          echo "::error::cpd@${version} never became installable on npm after ~5 minutes"
          exit 1

      - name: Fixture check
        working-directory: rust
        run: |
          cpd ../fixtures --silent --reporters json || true

      - name: Setup Node.js for publish
        uses: actions/setup-node@v5
        with:
          node-version: "22"
          registry-url: "https://registry.npmjs.org"
          package-manager-cache: false

      - name: Publish jscpd package
        working-directory: rust/jscpd
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          version="${{ needs.version.outputs.version }}"
          if npm view "jscpd@${version}" version >/dev/null 2>&1; then
            echo "jscpd@${version} is already published; skipping"
            exit 0
          fi
          npm publish --access public --provenance
