name: crates-publish

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

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

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

defaults:
  run:
    shell: bash
    working-directory: rust

jobs:
  version:
    name: resolve version
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      skip: ${{ steps.skip.outputs.skip }}
    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"

      - name: Check if already published
        id: skip
        working-directory: rust
        run: |
          version="${{ steps.version.outputs.version }}"
          # Use crates.io HTTP API for exact version match.
          # cargo info jscpd@ver exits 0 if the crate exists at ANY version.
          max_ver=$(curl -sfH "Accept: application/json" -H "User-Agent: jscpd-ci (github.com/kucherenko/jscpd)" "https://crates.io/api/v1/crates/jscpd" 2>/dev/null \
            | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const c=JSON.parse(d);process.stdout.write(c.crate.max_version)}catch(e){process.stdout.write('')}})" || echo "")
          if [ "$max_ver" = "$version" ] && [ -n "$max_ver" ]; then
            echo "jscpd@${version} is already published on crates.io (max_version=${max_ver}); skipping"
            echo "skip=true" >> "$GITHUB_OUTPUT"
          else
            echo "jscpd@${version} not yet on crates.io (max_version=${max_ver:-none}); will publish"
            echo "skip=false" >> "$GITHUB_OUTPUT"
          fi

  publish:
    name: publish crates.io packages
    needs: version
    if: needs.version.outputs.skip != 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout
        uses: actions/checkout@v5

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

      - name: Cache Cargo registry and target
        uses: Swatinem/rust-cache@v2
        with:
          workspaces: "rust -> rust/target"
          cache-on-failure: true

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

      - name: Install Node
        uses: actions/setup-node@v5
        with:
          node-version: "22"
          package-manager-cache: false

      - name: Sync version from package.json
        run: node scripts/sync-version.mjs

      - name: Install cargo-nextest
        uses: taiki-e/install-action@nextest

      - name: Test
        run: cargo nextest run --workspace --locked --profile ci

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: nextest-results-crates-publish
          path: rust/target/nextest/ci/nextest.xml
          if-no-files-found: warn

      - name: Test Report
        if: always()
        uses: dorny/test-reporter@v1
        with:
          name: Rust Tests (crates-publish)
          path: rust/target/nextest/ci/nextest.xml
          reporter: java-junit
          fail-on-error: false

      - name: Remove publish = false from all crates
        run: |
          find crates -name Cargo.toml -exec sed -i '/^publish = false$/d' {} \;
          sed -i '/^publish = false$/d' Cargo.toml
          echo "Removed publish = false from all crates"

      - name: Smoke test (build and version check)
        run: |
          cargo build --release --locked -p jscpd
          BUILT_VERSION=$(./target/release/cpd --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "NOT_FOUND")
          echo "Built version: ${BUILT_VERSION}"
          echo "Expected version: ${{ needs.version.outputs.version }}"
          [ "$BUILT_VERSION" = "${{ needs.version.outputs.version }}" ] || { echo "Version mismatch!"; exit 1; }

      - name: Authenticate to crates.io (Trusted Publishing)
        id: crates-io-auth
        uses: rust-lang/crates-io-auth-action@v1.0.4

      - name: Publish crates in dependency order
        working-directory: rust
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token || secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          # Discover workspace members in topological order (dependencies first)
          ORDER=$(cargo metadata --format-version 1 --no-deps 2>/dev/null | node -e "
          const meta = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
          const wsPkgs = meta.packages.filter(p => !p.id.includes('fuzz'));
          const byName = new Map(wsPkgs.map(p => [p.name, p]));
          const graph = new Map();
          for (const p of wsPkgs) {
            graph.set(p.name, p.dependencies
              .filter(d => d.path && byName.has(d.name))
              .map(d => d.name));
          }
          const sorted = [], visited = new Set();
          function visit(n) {
            if (visited.has(n)) return;
            visited.add(n);
            for (const dep of (graph.get(n) || [])) visit(dep);
            sorted.push(n);
          }
          for (const n of graph.keys()) visit(n);
          for (const name of sorted) {
            const pkg = byName.get(name);
            console.log(name + ':' + pkg.version);
          }
          ")

          echo "Publish order:"
          echo "$ORDER"

          for entry in $ORDER; do
            crate="${entry%%:*}"
            crate_version="${entry##*:}"

            echo "--- Publishing ${crate}@${crate_version} ---"

            # Use crates.io HTTP API for exact version check before publishing.
            # cargo info crate@ver exits 0 if the crate exists at ANY version,
            # not just the specified one — so we must compare max_version instead.
            max_ver=$(curl -sfH "Accept: application/json" -H "User-Agent: jscpd-ci (github.com/kucherenko/jscpd)" "https://crates.io/api/v1/crates/${crate}" 2>/dev/null \
              | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const c=JSON.parse(d);process.stdout.write(c.crate.max_version)}catch(e){process.stdout.write('')}})" || echo "")
            if [ "$max_ver" = "$crate_version" ] && [ -n "$max_ver" ]; then
              echo "${crate}@${crate_version} already published on crates.io (max_version=${max_ver}); skipping"
              continue
            fi

            if ! cargo publish --locked -p "${crate}" --allow-dirty; then
              echo "⚠ Publish failed for ${crate}@${crate_version} (may already exist); continuing"
              continue
            fi

            echo "Waiting for crates.io index to update for ${crate}@${crate_version}..."
            for i in $(seq 1 30); do
              # After publish, cargo info can verify the exact version since it now exists.
              if cargo info "${crate}@${crate_version}" >/dev/null 2>&1; then
                echo "${crate}@${crate_version} is available on crates.io"
                break
              fi
              echo "Attempt $i/30: waiting for index..."
              sleep 10
            done
          done