name: Build

on:
  workflow_dispatch:
  push:
    tags:
      - 'v*'

permissions:
  contents: read

jobs:
  build:
    strategy:
      matrix:
        include:
          - rid: osx-arm64
            name: officecli-mac-arm64
            os: macos-latest
          - rid: osx-x64
            name: officecli-mac-x64
            os: macos-latest
          - rid: linux-x64
            name: officecli-linux-x64
            os: ubuntu-latest
          - rid: linux-arm64
            name: officecli-linux-arm64
            os: ubuntu-latest
          - rid: linux-musl-x64
            name: officecli-linux-alpine-x64
            os: ubuntu-latest
          - rid: linux-musl-arm64
            name: officecli-linux-alpine-arm64
            os: ubuntu-latest
          - rid: win-x64
            name: officecli-win-x64.exe
            os: windows-latest
          - rid: win-arm64
            name: officecli-win-arm64.exe
            os: windows-latest
    runs-on: ${{ matrix.os }}

    defaults:
      run:
        shell: bash

    steps:
      - uses: actions/checkout@v5

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'

      - name: Publish
        run: dotnet publish src/officecli/officecli.csproj -c Release -r ${{ matrix.rid }} -o publish --nologo

      - name: Rename output
        run: |
          if [ -f publish/officecli.exe ]; then
            mv publish/officecli.exe publish/${{ matrix.name }}
          else
            mv publish/officecli publish/${{ matrix.name }}
          fi

      - name: Setup macOS code signing
        if: startsWith(matrix.rid, 'osx-')
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
          KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"

          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH"

          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security import "$CERT_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
          security set-key-partition-list -S apple-tool:,apple:,codesign: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db
          rm -f "$CERT_PATH"

      - name: Codesign (macOS)
        if: startsWith(matrix.rid, 'osx-')
        run: |
          IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -n 1 | sed -E 's/.*"(.+)"/\1/')
          echo "Signing with: $IDENTITY"
          # --options runtime (Hardened Runtime, required by notarization) denies the
          # self-contained CoreCLR's MAP_JIT executable memory by default, so the runtime
          # fails to start with "Failed to create CoreCLR, HRESULT: 0x80070008" on every
          # command. build/officecli.entitlements re-permits exactly allow-jit (the plist
          # holds no XML comments — codesign's AMFI parser rejects them).
          codesign --force --options runtime --entitlements build/officecli.entitlements --timestamp --sign "$IDENTITY" publish/${{ matrix.name }}
          codesign --verify --strict --verbose=2 publish/${{ matrix.name }}
          # Fail the build if allow-jit did not actually embed (e.g. a malformed plist
          # makes codesign silently drop entitlements) — otherwise the bug ships again.
          codesign -d --entitlements - --xml publish/${{ matrix.name }} | grep -q allow-jit

      - name: Notarize (macOS)
        if: startsWith(matrix.rid, 'osx-')
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
          TEAM_ID: ${{ secrets.TEAM_ID }}
        run: |
          # A bare Mach-O binary cannot be stapled; notarytool requires a
          # zip/pkg/dmg container. Submit a zip — the notarization ticket is
          # recorded on Apple's servers and Gatekeeper validates online.
          ditto -c -k --keepParent publish/${{ matrix.name }} "$RUNNER_TEMP/notarize.zip"
          xcrun notarytool submit "$RUNNER_TEMP/notarize.zip" \
            --apple-id "$APPLE_ID" \
            --team-id "$TEAM_ID" \
            --password "$APP_SPECIFIC_PASSWORD" \
            --wait
          rm -f "$RUNNER_TEMP/notarize.zip"

      - name: Smoke test - create document
        if: >-
          (matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') ||
          (matrix.rid == 'osx-x64' && runner.arch == 'X64') ||
          (matrix.rid == 'linux-x64' && runner.os == 'Linux') ||
          (matrix.rid == 'win-x64' && runner.os == 'Windows')
        env:
          # Disable Git Bash (MSYS) POSIX-to-Windows path conversion on
          # windows-latest, which otherwise mangles `/body` into
          # `C:/Program Files/Git/body` before it reaches the CLI.
          MSYS_NO_PATHCONV: '1'
          MSYS2_ARG_CONV_EXCL: '*'
        run: |
          chmod +x publish/${{ matrix.name }}
          publish/${{ matrix.name }} create test_smoke.docx
          publish/${{ matrix.name }} add test_smoke.docx /body --type paragraph --prop text="Hello from CI"
          publish/${{ matrix.name }} get test_smoke.docx '/body/p[1]'
          publish/${{ matrix.name }} close test_smoke.docx
          rm -f test_smoke.docx

      - name: Smoke test - .NET 8-only runtime (linux-x64)
        if: matrix.rid == 'linux-x64' && runner.os == 'Linux'
        run: |
          chmod +x publish/${{ matrix.name }}
          mkdir -p smoke_net8
          docker run --rm \
            -v "$PWD/publish:/app:ro" \
            -v "$PWD/smoke_net8:/work" \
            -w /work \
            mcr.microsoft.com/dotnet/runtime:8.0 \
            bash -c "set -e; \
              /app/${{ matrix.name }} create issue115.docx; \
              /app/${{ matrix.name }} add issue115.docx /body --type paragraph --prop text='net8 smoke'; \
              /app/${{ matrix.name }} close issue115.docx; \
              /app/${{ matrix.name }} view issue115.docx text --json"

      - name: Smoke test - install
        if: >-
          (matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') ||
          (matrix.rid == 'osx-x64' && runner.arch == 'X64') ||
          (matrix.rid == 'linux-x64' && runner.os == 'Linux') ||
          (matrix.rid == 'win-x64' && runner.os == 'Windows')
        env:
          MSYS_NO_PATHCONV: '1'
          MSYS2_ARG_CONV_EXCL: '*'
        run: |
          publish/${{ matrix.name }} install
          if [ "$RUNNER_OS" == "Windows" ]; then
            test -f "$LOCALAPPDATA/OfficeCLI/officecli.exe" || { echo "FAIL: officecli.exe not found in %LOCALAPPDATA%\\OfficeCLI"; exit 1; }
            "$LOCALAPPDATA/OfficeCLI/officecli.exe" --version
          else
            test -f "$HOME/.local/bin/officecli" || { echo "FAIL: officecli not found in ~/.local/bin"; exit 1; }
            "$HOME/.local/bin/officecli" --version
          fi

      - name: Smoke test - install.sh / install.ps1
        # Exercises the shell installers themselves (separate from the CLI's
        # own `install` subcommand tested above). Downloads from
        # d.officecli.ai with github fallback — validates the production
        # mirror is reachable AND the in-repo script is syntactically and
        # logically correct on each platform.
        if: >-
          (matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') ||
          (matrix.rid == 'osx-x64' && runner.arch == 'X64') ||
          (matrix.rid == 'linux-x64' && runner.os == 'Linux') ||
          (matrix.rid == 'win-x64' && runner.os == 'Windows')
        shell: bash
        env:
          MSYS_NO_PATHCONV: '1'
          MSYS2_ARG_CONV_EXCL: '*'
        run: |
          if [ "$RUNNER_OS" == "Windows" ]; then
            pwsh -NoProfile -ExecutionPolicy Bypass -File ./install.ps1
            test -f "$LOCALAPPDATA/OfficeCLI/officecli.exe" || { echo "FAIL: officecli.exe not installed"; exit 1; }
            "$LOCALAPPDATA/OfficeCLI/officecli.exe" --version
          else
            bash install.sh
            test -f "$HOME/.local/bin/officecli" || { echo "FAIL: officecli not installed at ~/.local/bin"; exit 1; }
            "$HOME/.local/bin/officecli" --version
          fi

      - name: Upload artifact
        uses: actions/upload-artifact@v6
        with:
          name: ${{ matrix.name }}
          path: publish/${{ matrix.name }}

  release:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    permissions:
      contents: write
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v8
        with:
          path: artifacts

      - name: Flatten artifacts and generate checksums
        run: |
          mkdir -p flat
          find artifacts -type f -exec mv {} flat/ \;
          rm -rf artifacts
          mv flat artifacts
          cd artifacts
          sha256sum officecli-* > SHA256SUMS
          echo "=== SHA256SUMS ==="
          cat SHA256SUMS

      - name: Create Draft Release
        uses: softprops/action-gh-release@v3
        with:
          files: artifacts/**/*
          generate_release_notes: true
          draft: true
