name: main

on:
  workflow_dispatch:
  pull_request:
  push:
    tags:
    - "*.*.*"
    branches:
    - "*"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
    - name: Determine version
      id: version
      run: |
        set -euxo pipefail
        BASE_VERSION=$(
          cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version'
        )
        if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
          VERSION="$BASE_VERSION"
        else
          VERSION="${BASE_VERSION}-dev.${GITHUB_RUN_NUMBER}+${GITHUB_SHA::8}"
        fi
        echo "version=$VERSION" >> "$GITHUB_OUTPUT"

  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: rustfmt, clippy
    - uses: Swatinem/rust-cache@v2
    - run: cargo fmt --check
    - run: cargo clippy -- -D warnings
    - run: cargo run -- --check

  test:
    strategy:
      fail-fast: false
      matrix:
        os:
        - ubuntu-latest
        - macos-latest
        - windows-latest
    runs-on: ${{ matrix.os }}
    timeout-minutes: 15
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2
    - run: cargo test

  build:
    needs: [version, lint, test]
    strategy:
      fail-fast: false
      matrix:
        include:
        - target: x86_64-unknown-linux-gnu
          os: ubuntu-latest
        - target: x86_64-unknown-linux-musl
          os: ubuntu-latest
        - target: aarch64-unknown-linux-gnu
          os: ubuntu-latest
        - target: aarch64-unknown-linux-musl
          os: ubuntu-latest
        - target: x86_64-apple-darwin
          os: macos-latest
        - target: aarch64-apple-darwin
          os: macos-latest
        - target: x86_64-pc-windows-msvc
          os: windows-latest
        - target: aarch64-pc-windows-msvc
          os: windows-latest
    runs-on: ${{ matrix.os }}
    timeout-minutes: 20
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
      with:
        targets: ${{ matrix.target }}
    - uses: Swatinem/rust-cache@v2
      with:
        key: ${{ matrix.target }}
    - if: matrix.target == 'x86_64-unknown-linux-musl'
      run: sudo apt-get update && sudo apt-get install -y musl-tools
    - if: matrix.target == 'aarch64-unknown-linux-gnu'
      run: |
        set -euxo pipefail
        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"
    - if: matrix.target == 'aarch64-unknown-linux-musl'
      uses: taiki-e/setup-cross-toolchain-action@v1
      with:
        target: ${{ matrix.target }}
    - name: Install dasel
      if: github.ref_type != 'tag'
      shell: bash
      env:
        DASEL_VERSION: "3.2.1"
      run: |
        set -euxo pipefail
        case "$RUNNER_OS" in
          Linux)   BINARY="dasel_linux_amd64" ;;
          macOS)   BINARY="dasel_darwin_arm64" ;;
          Windows) BINARY="dasel_windows_amd64.exe" ;;
        esac
        curl -sSLf \
          "https://github.com/TomWright/dasel/releases/download/v${DASEL_VERSION}/${BINARY}" \
          -o dasel
        chmod +x dasel
        echo "$PWD" >> "$GITHUB_PATH"
    - name: Set version in Cargo.toml
      if: github.ref_type != 'tag'
      shell: bash
      env:
        VERSION: ${{ needs.version.outputs.version }}
      run: |
        dasel -i toml --root "package.version = \"$VERSION\"" \
          < Cargo.toml > Cargo.toml.new
        mv Cargo.toml.new Cargo.toml
    - run: cargo build --release --target ${{ matrix.target }}
    - name: Create archive
      id: archive
      shell: bash
      env:
        VERSION: ${{ needs.version.outputs.version }}
      run: |
        set -euxo pipefail
        ARCHIVE_NAME="hongdown-$VERSION-${{ matrix.target }}"
        STAGING="hongdown-${{ matrix.target }}"
        mkdir -p "$STAGING"
        cp LICENSE README.md "$STAGING/"
        if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
          cp "target/${{ matrix.target }}/release/hongdown.exe" "$STAGING/"
          7z a "$ARCHIVE_NAME.zip" "$STAGING"
          echo "name=$ARCHIVE_NAME.zip" >> "$GITHUB_OUTPUT"
        else
          cp "target/${{ matrix.target }}/release/hongdown" "$STAGING/"
          tar -cjf "$ARCHIVE_NAME.tar.bz2" "$STAGING"
          echo "name=$ARCHIVE_NAME.tar.bz2" >> "$GITHUB_OUTPUT"
        fi
    - uses: actions/upload-artifact@v4
      with:
        name: ${{ steps.archive.outputs.name }}
        path: ${{ steps.archive.outputs.name }}

  release:
    if: github.event_name == 'push' && github.ref_type == 'tag'
    needs: [build]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
    - uses: actions/checkout@v4
    - uses: actions/download-artifact@v4
      with:
        path: artifacts
        merge-multiple: true
    - run: ls -la artifacts/
    - id: extract-changelog
      uses: dahlia/submark@5a5ff0a58382fb812616a5801402f5aef00f90ce
      with:
        input-file: CHANGES.md
        heading-level: 2
        heading-title-text: version ${{ github.ref_name }}
        ignore-case: true
        omit-heading: true
    - uses: softprops/action-gh-release@v2
      with:
        name: Hongdown ${{ github.ref_name }}
        body_path: ${{ steps.extract-changelog.outputs.output-file }}
        files: artifacts/*
        generate_release_notes: false

  publish-cargo:
    if: >-
      github.event_name == 'push' && (
        github.ref_type == 'tag' ||
        github.ref_name == 'main'
      )
    needs: [version, build]
    runs-on: ubuntu-latest
    environment:
      name: crates.io
      url: https://crates.io/crates/hongdown
    permissions:
      id-token: write
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2

    # For tag releases: verify Cargo.toml version matches tag exactly
    - name: Verify version matches tag
      if: github.ref_type == 'tag'
      env:
        VERSION: ${{ needs.version.outputs.version }}
      run: |
        set -euxo pipefail
        if [[ "$GITHUB_REF_NAME" != "$VERSION" ]]; then
          echo "::error::Tag '$GITHUB_REF_NAME' does not match Cargo.toml version '$VERSION'"
          exit 1
        fi
        echo "Version verified: $VERSION"

    # For dev releases: update Cargo.toml with dev version
    - name: Install dasel
      if: github.ref_type != 'tag'
      env:
        DASEL_VERSION: "3.2.1"
      run: |
        set -euxo pipefail
        curl -sSLf \
          "https://github.com/TomWright/dasel/releases/download/v${DASEL_VERSION}/dasel_linux_amd64" \
          -o dasel
        chmod +x dasel
        echo "$PWD" >> "$GITHUB_PATH"
    - name: Set dev version
      if: github.ref_type != 'tag'
      env:
        VERSION: ${{ needs.version.outputs.version }}
      run: |
        set -euxo pipefail
        dasel -i toml --root "package.version = \"$VERSION\"" \
          < Cargo.toml > Cargo.toml.new
        mv Cargo.toml.new Cargo.toml
        echo "Dev version set: $VERSION"
        cargo update --package hongdown

    - uses: rust-lang/crates-io-auth-action@v1
      id: crates-io-auth
    - run: cargo publish --allow-dirty
      env:
        CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }}

  publish-npm:
    if: >-
      github.event_name == 'push' && (
        github.ref_type == 'tag' ||
        github.ref_name == 'main'
      )
    needs: [version, build]
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
    - uses: actions/checkout@v4
    - uses: actions/download-artifact@v4
      with:
        path: artifacts
        merge-multiple: true
    - uses: actions/setup-node@v4
      with:
        node-version: '24'
        registry-url: https://registry.npmjs.org

    - name: Install dasel
      env:
        DASEL_VERSION: "3.2.1"
      run: |
        set -euxo pipefail
        curl -sSLf \
          "https://github.com/TomWright/dasel/releases/download/v${DASEL_VERSION}/dasel_linux_amd64" \
          -o dasel
        chmod +x dasel
        echo "$PWD" >> "$GITHUB_PATH"
    - name: Prepare npm packages
      env:
        VERSION: ${{ needs.version.outputs.version }}
      run: |
        set -euxo pipefail
        cd npm

        # Update version in main package
        dasel -i json --root "version = \"$VERSION\"" \
          < package.json > package.json.tmp
        mv package.json.tmp package.json

        # Update optionalDependencies versions
        for pkg in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-arm64 win32-x64; do
          dasel -i json --root "optionalDependencies[\"@hongdown/$pkg\"] = \"$VERSION\"" \
            < package.json > package.json.tmp
          mv package.json.tmp package.json
        done

        # Copy README
        cp ../README.md .

        # Platform mappings: npm_pkg:rust_target:os:cpu
        platforms=(
          "darwin-arm64:aarch64-apple-darwin:darwin:arm64"
          "darwin-x64:x86_64-apple-darwin:darwin:x64"
          "linux-arm64:aarch64-unknown-linux-musl:linux:arm64"
          "linux-x64:x86_64-unknown-linux-musl:linux:x64"
          "win32-arm64:aarch64-pc-windows-msvc:win32:arm64"
          "win32-x64:x86_64-pc-windows-msvc:win32:x64"
        )

        for entry in "${platforms[@]}"; do
          IFS=':' read -r npm_pkg rust_target os cpu <<< "$entry"

          mkdir -p "$npm_pkg/bin"

          # Generate package.json
          cat > "$npm_pkg/package.json" << EOF
        {
          "name": "@hongdown/$npm_pkg",
          "version": "$VERSION",
          "description": "Hongdown binary for $npm_pkg",
          "license": "GPL-3.0-or-later",
          "author": "Hong Minhee <hong@minhee.org>",
          "repository": {
            "type": "git",
            "url": "git+https://github.com/dahlia/hongdown.git"
          },
          "os": ["$os"],
          "cpu": ["$cpu"],
          "files": ["bin/"]
        }
        EOF

          # Extract binary from archive
          archive="../artifacts/hongdown-$VERSION-$rust_target"
          if [[ "$os" == "win32" ]]; then
            unzip -q "${archive}.zip" -d "$npm_pkg/"
            mv "$npm_pkg/hongdown-$rust_target/hongdown.exe" "$npm_pkg/bin/"
          else
            tar -xjf "${archive}.tar.bz2" -C "$npm_pkg/"
            mv "$npm_pkg/hongdown-$rust_target/hongdown" "$npm_pkg/bin/"
            chmod +x "$npm_pkg/bin/hongdown"
          fi
          rm -rf "$npm_pkg/hongdown-$rust_target"
        done

    - name: Publish to npm
      env:
        VERSION: ${{ needs.version.outputs.version }}
      run: |
        set -euxo pipefail
        cd npm

        # Determine tag for dev versions
        if [[ "$VERSION" == *"-dev."* ]]; then
          TAG="--tag dev"
        else
          TAG=""
        fi

        # Publish platform packages first
        for pkg in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-arm64 win32-x64; do
          cd "$pkg"
          npm publish --access public --provenance $TAG
          cd ..
        done

        # Publish main package
        npm publish --access public --provenance $TAG
