name: Release

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

permissions:
  contents: write

env:
  # Prevent Cargo from stripping binaries automatically.
  # We need unstripped binaries for the "Full" artifacts (debug symbols/PDBs).
  CARGO_PROFILE_RELEASE_STRIP: false
  # Map secret existence to env vars to avoid context issues in 'if' conditions
  HAS_GPG_SECRET: ${{ secrets.GPG_PRIVATE_KEY != '' }}
  HAS_PUBLIC_KEY: ${{ vars.GPG_PUBLIC_KEY != '' }}

jobs:
  # ===========================================================================
  # JOB 1: Build Matrix (Linux, Windows, macOS Single-Arch)
  # ===========================================================================
  build-matrix:
    name: Build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # --- Linux (GNU) ---
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            use_cross: false
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            use_cross: true
          - os: ubuntu-latest
            target: armv7-unknown-linux-gnueabihf
            use_cross: true

          # --- Linux (MUSL) ---
          - os: ubuntu-latest
            target: x86_64-unknown-linux-musl
            use_cross: true
          - os: ubuntu-latest
            target: aarch64-unknown-linux-musl
            use_cross: true
          - os: ubuntu-latest
            target: armv7-unknown-linux-musleabihf
            use_cross: true

          # --- macOS ---
          - os: macos-latest
            target: x86_64-apple-darwin
            use_cross: false
          - os: macos-14 # Apple Silicon runner
            target: aarch64-apple-darwin
            use_cross: false

          # --- Windows ---
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            use_cross: false
          - os: windows-latest
            target: aarch64-pc-windows-msvc
            use_cross: false

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: stable
          target: ${{ matrix.target }}
          cache: 'cargo'

      # --- Auto-detect Binary Name ---
      - name: Get Binary Name
        id: bin_name
        shell: bash
        run: |
          # Parses Cargo.toml metadata to find the name of the first "bin" target.
          # This handles cases where package.name != binary.name
          BIN_NAME=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].targets[] | select(.kind[] | contains("bin")) | .name' | head -n 1)
          if [ -z "$BIN_NAME" ]; then
            echo "Error: Could not determine binary name from Cargo.toml"
            exit 1
          fi
          echo "Detected binary: $BIN_NAME"
          echo "BINARY_NAME=$BIN_NAME" >> $GITHUB_ENV

      - name: Install Cross
        if: matrix.use_cross
        uses: taiki-e/install-action@v2
        with:
          tool: cross

      - name: Build Binary
        shell: bash
        run: |
          CMD="cargo"
          if [[ "${{ matrix.use_cross }}" == "true" ]]; then CMD="cross"; fi
          $CMD build --release --target ${{ matrix.target }} --verbose

      # Only import GPG if the secret is actually set.
      - name: Import GPG Key
        if: env.HAS_GPG_SECRET == 'true'
        uses: crazy-max/ghaction-import-gpg@v6
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          passphrase: ${{ secrets.GPG_PASSPHRASE }}

      - name: Package and Sign
        shell: bash
        env:
          TARGET: ${{ matrix.target }}
          REF_NAME: ${{ github.ref_name }}
          HAS_GPG: ${{ secrets.GPG_PRIVATE_KEY != '' }}
        run: |
          # --- Setup Variables ---
          # Ensure version always has 'v' prefix for folder naming (e.g., v1.0.0).
          CLEAN_VERSION="${REF_NAME#v}"
          VERSION="v${CLEAN_VERSION}"
          
          # Normalize binary name (Rust replaces hyphens with underscores).
          BIN_FILE="${BINARY_NAME//-/_}"
          
          # Determine extensions based on OS.
          BIN_EXT=""
          ARCHIVE_EXT="tar.gz"
          if [[ "$TARGET" == *"windows"* ]]; then
            BIN_EXT=".exe"
            ARCHIVE_EXT="zip"
          elif [[ "$TARGET" == *"apple"* ]]; then
            ARCHIVE_EXT="zip"
          fi

          # Locate the built binary.
          BUILD_DIR="target/$TARGET/release"
          BIN_PATH="$BUILD_DIR/$BIN_FILE$BIN_EXT"

          if [[ ! -f "$BIN_PATH" ]]; then
            echo "Error: Binary not found at $BIN_PATH"
            exit 1
          fi

          # --- Prepare Directories (binstall compliance) ---
          # Structure: {name}-{target}-{version}/
          DIR_NAME="${BINARY_NAME}-${TARGET}-${VERSION}"
          FULL_DIR_NAME="${BINARY_NAME}-${TARGET}-${VERSION}-full"
          
          mkdir -p "$DIR_NAME" "$FULL_DIR_NAME"

          # --- Populate "Full" Directory (Unstripped + PDB) ---
          cp "$BIN_PATH" "$FULL_DIR_NAME/$BINARY_NAME$BIN_EXT"
          cp LICENSE README.md "$FULL_DIR_NAME/"
          # On Windows, include the PDB debug symbols.
          if [[ "$TARGET" == *"windows"* ]]; then
            cp "$BUILD_DIR/$BIN_FILE.pdb" "$FULL_DIR_NAME/$BINARY_NAME.pdb" || true
          fi

          # --- Populate "Standard" Directory (Stripped) ---
          cp "$BIN_PATH" "$DIR_NAME/$BINARY_NAME$BIN_EXT"
          cp LICENSE README.md "$DIR_NAME/"
          
          # Manually strip the binary for the standard release to reduce size.
          if [[ "$TARGET" != *"windows"* ]]; then
            if [[ "$TARGET" == *"apple"* ]]; then
              strip -x "$DIR_NAME/$BINARY_NAME$BIN_EXT" || true
            else
              strip "$DIR_NAME/$BINARY_NAME$BIN_EXT" || true
            fi
          fi

          # --- Create Archives ---
          FULL_ARCHIVE="${FULL_DIR_NAME}.${ARCHIVE_EXT}"
          STD_ARCHIVE="${DIR_NAME}.${ARCHIVE_EXT}"

          if [[ "$ARCHIVE_EXT" == "zip" ]]; then
            if command -v zip &> /dev/null; then
              zip -r "$FULL_ARCHIVE" "$FULL_DIR_NAME"
              zip -r "$STD_ARCHIVE" "$DIR_NAME"
            elif command -v 7z &> /dev/null; then
              7z a "$FULL_ARCHIVE" "$FULL_DIR_NAME"
              7z a "$STD_ARCHIVE" "$DIR_NAME"
            else
              echo "Error: Neither 'zip' nor '7z' found."
              exit 1
            fi
          else
            tar -czf "$FULL_ARCHIVE" "$FULL_DIR_NAME"
            tar -czf "$STD_ARCHIVE" "$DIR_NAME"
          fi

          # --- Sign Archives (Optional) ---
          if [[ "$HAS_GPG" == "true" ]]; then
            gpg --batch --yes --detach-sign --armor --output "${FULL_ARCHIVE}.sig" "$FULL_ARCHIVE"
            gpg --batch --yes --detach-sign --armor --output "${STD_ARCHIVE}.sig" "$STD_ARCHIVE"
          fi

          # --- Organize for Upload ---
          mkdir dist
          mv "${FULL_ARCHIVE}" "${STD_ARCHIVE}" dist/
          if [[ "$HAS_GPG" == "true" ]]; then
            mv "${FULL_ARCHIVE}.sig" "${STD_ARCHIVE}.sig" dist/
          fi

      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: artifacts-${{ matrix.target }}
          path: dist/*

  # ===========================================================================
  # JOB 2: Build Universal macOS Binary
  # ===========================================================================
  build-mac-universal:
    name: Build universal-apple-darwin
    needs: build-matrix
    runs-on: macos-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # We need Rust installed here just to run 'cargo metadata' to get the name.
      - name: Install Rust toolchain
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: stable
          cache: 'cargo'

      # --- Auto-detect Binary Name ---
      - name: Get Binary Name
        id: bin_name
        shell: bash
        run: |
          BIN_NAME=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].targets[] | select(.kind[] | contains("bin")) | .name' | head -n 1)
          echo "Detected binary: $BIN_NAME"
          echo "BINARY_NAME=$BIN_NAME" >> $GITHUB_ENV

      - name: Download x86_64 Artifacts
        uses: actions/download-artifact@v4
        with:
          name: artifacts-x86_64-apple-darwin
          path: artifacts-x86

      - name: Download arm64 Artifacts
        uses: actions/download-artifact@v4
        with:
          name: artifacts-aarch64-apple-darwin
          path: artifacts-arm

      - name: Create Universal Binary (Lipo)
        shell: bash
        run: |
          # Extract the "Full" (unstripped) zips to get the raw binaries.
          unzip -o artifacts-x86/*x86_64-apple-darwin*-full.zip -d x86_extract
          unzip -o artifacts-arm/*aarch64-apple-darwin*-full.zip -d arm_extract
          
          # Find the binary files (ignoring folder structure).
          BIN_X86=$(find x86_extract -type f -name "${BINARY_NAME}" | head -n 1)
          BIN_ARM=$(find arm_extract -type f -name "${BINARY_NAME}" | head -n 1)

          if [[ -z "$BIN_X86" || -z "$BIN_ARM" ]]; then
            echo "Error: Could not locate extracted binaries for lipo."
            exit 1
          fi

          # Merge architectures.
          mkdir -p output
          lipo -create -output output/${BINARY_NAME} "$BIN_X86" "$BIN_ARM"

      - name: Import GPG Key
        if: env.HAS_GPG_SECRET == 'true'
        uses: crazy-max/ghaction-import-gpg@v6
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          passphrase: ${{ secrets.GPG_PASSPHRASE }}

      - name: Package and Sign
        shell: bash
        env:
          HAS_GPG: ${{ secrets.GPG_PRIVATE_KEY != '' }}
          REF_NAME: ${{ github.ref_name }}
        run: |
          CLEAN_VERSION="${REF_NAME#v}"
          VERSION="v${CLEAN_VERSION}"
          
          TARGET="universal-apple-darwin"
          BIN_PATH="output/${BINARY_NAME}"
          
          # --- Prepare Directories ---
          DIR_NAME="${BINARY_NAME}-${TARGET}-${VERSION}"
          FULL_DIR_NAME="${BINARY_NAME}-${TARGET}-${VERSION}-full"
          
          mkdir -p "$DIR_NAME" "$FULL_DIR_NAME"

          # Full (Unstripped)
          cp "$BIN_PATH" "$FULL_DIR_NAME/$BINARY_NAME"
          cp LICENSE README.md "$FULL_DIR_NAME/"
          
          # Standard (Stripped)
          cp "$BIN_PATH" "$DIR_NAME/$BINARY_NAME"
          cp LICENSE README.md "$DIR_NAME/"
          strip -x "$DIR_NAME/$BINARY_NAME"

          # --- Create Archives ---
          FULL_ARCHIVE="${FULL_DIR_NAME}.zip"
          STD_ARCHIVE="${DIR_NAME}.zip"
          
          zip -r "$FULL_ARCHIVE" "$FULL_DIR_NAME"
          zip -r "$STD_ARCHIVE" "$DIR_NAME"

          # --- Sign Archives ---
          if [[ "$HAS_GPG" == "true" ]]; then
            gpg --batch --yes --detach-sign --armor --output "${FULL_ARCHIVE}.sig" "$FULL_ARCHIVE"
            gpg --batch --yes --detach-sign --armor --output "${STD_ARCHIVE}.sig" "$STD_ARCHIVE"
          fi

          # --- Organize for Upload ---
          mkdir dist
          mv "${FULL_ARCHIVE}" "${STD_ARCHIVE}" dist/
          if [[ "$HAS_GPG" == "true" ]]; then
             mv "${FULL_ARCHIVE}.sig" "${STD_ARCHIVE}.sig" dist/
          fi

      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: artifacts-universal
          path: dist/*

  # ===========================================================================
  # JOB 3: Create Release
  # ===========================================================================
  create-release:
    name: Create GitHub Release
    needs: [build-matrix, build-mac-universal]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          # Fetch full history so we can find the previous tag for comparison links
          fetch-depth: 0

      - name: Download All Artifacts
        uses: actions/download-artifact@v4
        with:
          path: release-assets
          merge-multiple: true

      - name: Include Public Key
        # Only run if the variable is set in GitHub Settings
        if: env.HAS_PUBLIC_KEY == 'true'
        run: |
          # Write the variable content to a file in the assets folder
          echo "${{ vars.GPG_PUBLIC_KEY }}" > release-assets/public.key

      - name: Install parse-changelog
        uses: taiki-e/install-action@v2
        with:
          tool: parse-changelog

      - name: Generate Release Notes
        id: notes
        shell: bash
        run: |
          TAG_NAME="${{ github.ref_name }}"
          
          # 1. Generate both version formats (with 'v' and without)
          # Strip existing 'v' prefix if present to get the raw number
          CLEAN_VERSION="${TAG_NAME#v}"
          
          VERSION_WITH_V="v${CLEAN_VERSION}"  # e.g., v1.0.0
          VERSION_NO_V="${CLEAN_VERSION}"     # e.g., 1.0.0
          
          echo "Searching CHANGELOG.md for '$VERSION_WITH_V' or '$VERSION_NO_V'..."

          # 2. Attempt parse: Try 'v' format first, then 'no-v' format
          if parse-changelog CHANGELOG.md "$VERSION_WITH_V" > release_notes.md 2>/dev/null; then
            echo "✅ Found entry using format: $VERSION_WITH_V"
            FOUND=true
          elif parse-changelog CHANGELOG.md "$VERSION_NO_V" > release_notes.md 2>/dev/null; then
            echo "✅ Found entry using format: $VERSION_NO_V"
            FOUND=true
          else
            echo "⚠️ No entry found in CHANGELOG.md"
            FOUND=false
          fi

          # 3. Finalize Output
          if [ "$FOUND" == "true" ]; then
            # --- Append Compare Link ---
            # We use the actual git tag (TAG_NAME) for the URL, regardless of changelog format
            PREV_TAG=$(git describe --abbrev=0 --tags "${TAG_NAME}^" 2>/dev/null || true)
            
            if [ -n "$PREV_TAG" ]; then
              REPO_URL="${{ github.server_url }}/${{ github.repository }}"
              echo -e "\n\n---" >> release_notes.md
              echo "🆚 [Compare changes](${REPO_URL}/compare/${PREV_TAG}...${TAG_NAME})" >> release_notes.md
            fi
            
            echo "use_changelog=true" >> $GITHUB_OUTPUT
          else
            echo "Falling back to GitHub auto-generated notes."
            echo "use_changelog=false" >> $GITHUB_OUTPUT
          fi

      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          # If use_changelog is true, use the file. Otherwise, leave empty.
          body_path: ${{ steps.notes.outputs.use_changelog == 'true' && 'release_notes.md' || '' }}
          # If use_changelog is NOT true, generate notes automatically.
          generate_release_notes: ${{ steps.notes.outputs.use_changelog != 'true' }}
          files: release-assets/*
          draft: true
          prerelease: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
