name: Release

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

permissions:
  actions: read
  contents: write

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
  PYTHON_BINDING_FEATURES: >-
    parallel
    alpha-dft alpha-reaxff alpha-mlff alpha-obara-saika alpha-cga alpha-gsm alpha-sdr
    beta-kpm beta-mbh beta-randnla beta-riemannian beta-cpm
  WASM_WEB_FEATURES: >-
    parallel experimental-gpu
    alpha-dft alpha-reaxff alpha-mlff alpha-obara-saika alpha-cga alpha-gsm alpha-sdr
    alpha-dynamics-live alpha-imd
    beta-kpm beta-mbh beta-randnla beta-riemannian beta-cpm
  WASM_NODE_FEATURES: >-
    alpha-dft alpha-reaxff alpha-mlff alpha-obara-saika alpha-cga alpha-gsm alpha-sdr
    alpha-dynamics-live alpha-imd
    beta-kpm beta-mbh beta-randnla beta-riemannian beta-cpm

# ─────────────────────────────────────────────────────────────────────────────
# Release pipeline:
#   1. wait-for-ci        — block until CI passes for the same SHA
#   2. build-cli          — cross-compile CLI grouped by OS (3 runners, 5 targets)
#   3. build-python       — maturin wheels (3 platforms × 3.11)
#   4. build-wasm         — wasm-pack bundler + npm package validation
#   5. verify-python      — install wheel + smoke test
#   6. verify-node        — install npm pkg + smoke test
#   7. verify-cli-linux   — run the linux binary + smoke test
#   8. publish-crate      — cargo publish to crates.io
#   9. publish-python     — twine upload to PyPI
#  10. publish-npm        — npm publish
#  11. release            — GitHub Release with all artifacts
# ─────────────────────────────────────────────────────────────────────────────

jobs:
  # ─── Gate 1: Wait for CI on the same SHA ───────────────────────────────────
  wait-for-ci:
    name: Wait for CI
    runs-on: ubuntu-latest
    steps:
      - name: Wait for CI workflow to finish successfully
        uses: actions/github-script@v7
        with:
          script: |
            const workflowId = 'ci.yml';
            const sha = context.sha;
            const timeoutMs = 60 * 60 * 1000;
            const intervalMs = 30 * 1000;
            const deadline = Date.now() + timeoutMs;

            while (Date.now() < deadline) {
              const { data } = await github.rest.actions.listWorkflowRuns({
                owner: context.repo.owner,
                repo: context.repo.repo,
                workflow_id: workflowId,
                head_sha: sha,
                per_page: 10,
              });

              const run = data.workflow_runs.find((item) => item.head_sha === sha);

              if (!run) {
                core.info(`No CI run found yet for ${sha}; waiting...`);
              } else if (run.status !== 'completed') {
                core.info(`CI run ${run.id} is ${run.status}; waiting...`);
              } else if (run.conclusion === 'success') {
                core.info(`CI passed for ${sha}: ${run.html_url}`);
                return;
              } else {
                throw new Error(`CI run ${run.id} finished with ${run.conclusion}: ${run.html_url}`);
              }

              await new Promise((resolve) => setTimeout(resolve, intervalMs));
            }

            throw new Error(`Timed out waiting for CI to complete successfully for ${sha}`);

  # ─── CLI Binaries (grouped by OS to reduce runner startup overhead) ───────
  build-cli-linux:
    name: CLI (ubuntu-latest)
    needs: [wait-for-ci]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu
      - uses: Swatinem/rust-cache@v2
      - name: Install cross-compilation tools (aarch64-linux)
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
      - name: Build Linux x86_64 CLI
        run: cargo build --release --package sci-form-cli --target x86_64-unknown-linux-gnu
      - name: Build Linux aarch64 CLI
        run: cargo build --release --package sci-form-cli --target aarch64-unknown-linux-gnu
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
      - name: Stage Linux x86_64 artifact
        shell: bash
        run: cp target/x86_64-unknown-linux-gnu/release/sci-form sci-form-linux-x86_64
      - name: Stage Linux aarch64 artifact
        shell: bash
        run: cp target/aarch64-unknown-linux-gnu/release/sci-form sci-form-linux-aarch64
      - uses: actions/upload-artifact@v6
        with:
          name: sci-form-linux-x86_64
          path: sci-form-linux-x86_64
      - uses: actions/upload-artifact@v6
        with:
          name: sci-form-linux-aarch64
          path: sci-form-linux-aarch64

  build-cli-macos:
    name: CLI (macos-latest)
    needs: [wait-for-ci]
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-apple-darwin,aarch64-apple-darwin
      - uses: Swatinem/rust-cache@v2
      - name: Build macOS x86_64 CLI
        run: cargo build --release --package sci-form-cli --target x86_64-apple-darwin
      - name: Build macOS aarch64 CLI
        run: cargo build --release --package sci-form-cli --target aarch64-apple-darwin
      - name: Stage macOS x86_64 artifact
        shell: bash
        run: cp target/x86_64-apple-darwin/release/sci-form sci-form-macos-x86_64
      - name: Stage macOS aarch64 artifact
        shell: bash
        run: cp target/aarch64-apple-darwin/release/sci-form sci-form-macos-aarch64
      - uses: actions/upload-artifact@v6
        with:
          name: sci-form-macos-x86_64
          path: sci-form-macos-x86_64
      - uses: actions/upload-artifact@v6
        with:
          name: sci-form-macos-aarch64
          path: sci-form-macos-aarch64

  build-cli-windows:
    name: CLI (windows-latest)
    needs: [wait-for-ci]
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-pc-windows-msvc
      - uses: Swatinem/rust-cache@v2
      - name: Build Windows CLI
        run: cargo build --release --package sci-form-cli --target x86_64-pc-windows-msvc
      - name: Stage Windows artifact
        shell: bash
        run: cp target/x86_64-pc-windows-msvc/release/sci-form.exe sci-form-windows-x86_64.exe
      - uses: actions/upload-artifact@v6
        with:
          name: sci-form-windows-x86_64.exe
          path: sci-form-windows-x86_64.exe

  # ─── Python Wheels ─────────────────────────────────────────────────────────
  build-python:
    name: Python (${{ matrix.os }})
    needs: [wait-for-ci]
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.11"
          cache: pip
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Install maturin
        run: pip install maturin
      - name: Build wheel
        run: cd crates/python && maturin build --release --features "${PYTHON_BINDING_FEATURES}"
      - uses: actions/upload-artifact@v6
        with:
          name: python-wheel-${{ matrix.os }}
          path: target/wheels/*.whl

  # ─── WASM / npm ────────────────────────────────────────────────────────────
  build-wasm:
    name: WASM / npm
    needs: [wait-for-ci]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
      - uses: Swatinem/rust-cache@v2
      - name: Install wasm-pack
        run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
      - name: Build WASM packages (web + nodejs)
        run: cd crates/wasm && bash build.sh --web-features "${WASM_WEB_FEATURES}" --node-features "${WASM_NODE_FEATURES}"
      - name: Check alpha/beta subpath packaging
        run: |
          test -f crates/wasm/pkg/alpha/index.js
          test -f crates/wasm/pkg/alpha/index.d.ts
          test -f crates/wasm/pkg/beta/index.js
          test -f crates/wasm/pkg/beta/index.d.ts
      - name: Verify Node.js install
        run: cd crates/wasm/pkg-node && node -e "const sci = require('./sci_form_wasm.js'); const alpha = require('./alpha'); const beta = require('./beta'); const r = JSON.parse(sci.embed('CCO', 42)); console.assert(r.num_atoms === 9, 'Expected 9 atoms'); console.assert(typeof sci.alpha_compute_dft === 'function', 'missing alpha root export'); console.assert(typeof sci.beta_compute_kpm_dos === 'function', 'missing beta root export'); console.assert(typeof alpha.alpha_modules_info === 'function', 'missing alpha subpath export'); console.assert(typeof beta.beta_modules_info === 'function', 'missing beta subpath export'); console.log('✓ Node.js WASM works with alpha/beta exports');"
      - uses: actions/upload-artifact@v6
        with:
          name: wasm-pkg
          path: crates/wasm/pkg/
      - uses: actions/upload-artifact@v6
        with:
          name: wasm-pkg-node
          path: crates/wasm/pkg-node/

  # ─── Verify: Python wheel installs cleanly ─────────────────────────────────
  verify-python:
    name: Verify Python install
    needs: [build-python]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.11"
      - uses: actions/download-artifact@v8
        with:
          name: python-wheel-ubuntu-latest
          path: wheels/
      - name: Install wheel
        run: pip install wheels/*.whl
      - name: Smoke test — embed caffeine
        run: |
          python3 -c "
          import sci_form
          result = sci_form.embed('Cn1cnc2c1c(=O)n(c(=O)n2C)C', 42)
          assert result.num_atoms > 0, 'embed failed'
          assert len(result.coords) == result.num_atoms * 3, 'coords size mismatch'
          assert result.error is None, f'error: {result.error}'
          print(f'OK: {result.num_atoms} atoms embedded')
          "
      - name: Smoke test — batch embed
        run: |
          python3 -c "
          import sci_form
          smiles = ['CCO', 'c1ccccc1', 'CC(=O)O', 'C1CCCCC1']
          results = [sci_form.embed(s, 42) for s in smiles]
          failed = [r.smiles for r in results if r.error is not None]
          assert not failed, f'Failed: {failed}'
          print(f'OK: {len(results)} molecules embedded')
          "
      - name: Smoke test — experimental Python bindings
        run: |
          python3 -c "
          from sci_form.alpha import dft_calculate, reaxff_gradient, alpha_compute_aevs
          from sci_form.beta import kpm_dos, eht_randnla, cpm_charges
          import sci_form
          result = sci_form.embed('CCO', 42)
          assert result.error is None, result.error
          dft = dft_calculate(result.elements, result.coords, 'pbe')
          reax = reaxff_gradient(result.elements, result.coords)
          aevs = alpha_compute_aevs(result.elements, result.coords)
          kpm = kpm_dos(result.elements, result.coords, order=64)
          randnla = eht_randnla(result.elements, result.coords, sketch_size=8)
          cpm = cpm_charges(result.elements, result.coords, potential=0.0)
          assert dft.n_basis > 0
          assert len(reax.gradient) == len(result.coords)
          assert aevs.n_atoms == result.num_atoms
          assert len(kpm.energies) > 0
          assert len(randnla.orbital_energies) > 0
          assert len(cpm.charges) == result.num_atoms
          print('OK: experimental Python bindings work')
          "

  # ─── Verify: WASM/Node package installs cleanly ───────────────────────────
  verify-node:
    name: Verify Node.js install
    needs: [build-wasm]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
      - uses: actions/download-artifact@v8
        with:
          name: wasm-pkg-node
          path: pkg-node/
      - name: Smoke test — embed caffeine (Node.js CJS)
        run: |
          node -e "
          const sci = require('./pkg-node/sci_form_wasm.js');
          const alpha = require('./pkg-node/alpha');
          const beta = require('./pkg-node/beta');
          const result = JSON.parse(sci.embed('Cn1cnc2c1c(=O)n(c(=O)n2C)C', 42));
          if (result.error) throw new Error('embed failed: ' + result.error);
          if (result.num_atoms !== 24) throw new Error('expected 24 atoms, got ' + result.num_atoms);
          if (typeof sci.alpha_compute_dft !== 'function') throw new Error('missing alpha root export');
          if (typeof sci.beta_compute_kpm_dos !== 'function') throw new Error('missing beta root export');
          if (typeof alpha.alpha_modules_info !== 'function') throw new Error('missing alpha subpath export');
          if (typeof beta.beta_modules_info !== 'function') throw new Error('missing beta subpath export');
          console.log('OK: ' + result.num_atoms + ' atoms embedded');
          "

  # ─── Verify: CLI binary runs ───────────────────────────────────────────────
  verify-cli:
    name: Verify CLI binary
    needs: [build-cli-linux]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: sci-form-linux-x86_64
          path: .
      - name: Make executable
        run: chmod +x sci-form-linux-x86_64
      - name: Smoke test — version
        run: ./sci-form-linux-x86_64 --version
      - name: Smoke test — embed caffeine
        run: |
          ./sci-form-linux-x86_64 embed "Cn1cnc2c1c(=O)n(c(=O)n2C)C" \
            | grep -E '"num_atoms"' || (echo "CLI embed failed"; exit 1)
      - name: Smoke test — parse
        run: |
          ./sci-form-linux-x86_64 parse "CCO" \
            | grep -iE 'Atoms:' || (echo "CLI parse failed"; exit 1)

  # ─── Publish: crates.io ────────────────────────────────────────────────────
  publish-crate:
    name: Publish to crates.io
    needs: [verify-python, verify-node, verify-cli]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Publish sci-form (lib)
        run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
      - name: Wait for crates.io propagation
        run: sleep 30
      - name: Publish sci-form-cli
        run: cd crates/cli && cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }}

  # ─── Publish: PyPI ─────────────────────────────────────────────────────────
  publish-python:
    name: Publish to PyPI
    needs: [verify-python]
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v8
        with:
          pattern: python-wheel-*
          path: dist/
          merge-multiple: true
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          password: ${{ secrets.PYPI_API_TOKEN }}
          skip-existing: true
          verbose: true

  # ─── Publish: npm ──────────────────────────────────────────────────────────
  # NOTE: NPM_TOKEN must be a Granular Access Token with Automation type.
  # Classic tokens require OTP even in CI. To create one:
  #   npmjs.com → Avatar → Access Tokens → Generate New Token
  #   → Granular Access Token → Expiration → Packages: sci-form-wasm (Read+Write)
  #   → Token type: Automation → Generate Token
  # Then update the NPM_TOKEN repository secret with the new token.
  publish-npm:
    name: Publish to npm
    needs: [verify-node]
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: wasm-pkg
          path: pkg/
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org
      - name: Publish
        run: cd pkg && npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  # ─── GitHub Release ────────────────────────────────────────────────────────
  release:
    name: Create GitHub Release
    needs: [publish-crate, publish-python, publish-npm]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/download-artifact@v8
        with:
          path: artifacts/
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: |
            artifacts/sci-form-linux-x86_64/*
            artifacts/sci-form-linux-aarch64/*
            artifacts/sci-form-macos-x86_64/*
            artifacts/sci-form-macos-aarch64/*
            artifacts/sci-form-windows-x86_64.exe/*
            artifacts/python-wheel-*/*.whl
          generate_release_notes: true
          body: |
            ## Installation

            ### Python
            ```bash
            pip install sciforma
            ```

            ### Node.js / TypeScript
            ```bash
            npm install sci-form-wasm
            ```

            ### Rust
            ```toml
            [dependencies]
            sci-form = "0.1"
            ```

            ### CLI
            Download the binary for your platform from the assets below.
            - **Linux x86_64**: `sci-form-linux-x86_64`
            - **Linux aarch64**: `sci-form-linux-aarch64`
            - **macOS x86_64**: `sci-form-macos-x86_64`
            - **macOS Apple Silicon**: `sci-form-macos-aarch64`
            - **Windows x86_64**: `sci-form-windows-x86_64.exe`


