# ─────────────────────────────────────────────────────────────────────────────
# release.yml  —  Build cross-platform binaries + Docker image on git tag push
#
# Trigger: push of a semver tag  vX.Y.Z
#
# Jobs:
#   1. test         — run full test suite (gate for everything else)
#   2. build        — cross-compile binaries for 6 targets  ┐ run in parallel
#   3. docker       — build & push multi-arch image          │ after test passes
#   4. changelog    — generate CHANGELOG.md from git log     ┘
#   5. github-release — create GH release, attach all binaries + changelog
# ─────────────────────────────────────────────────────────────────────────────
name: Release

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"
      - "v[0-9]+.[0-9]+.[0-9]+-*"   # also pre-releases: v1.2.3-rc1

permissions:
  contents: write        # create releases, upload assets
  packages: write        # push to GHCR
  id-token: write        # cosign keyless signing + crates.io OIDC (rust-lang/crates-io-auth-action)

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ── 1. Gate: run tests before building anything ────────────────────────────
  test:
    name: Test (release gate)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

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

      - name: Cache cargo
        uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-release-test-${{ hashFiles('**/Cargo.lock') }}

      - name: Run tests
        run: cargo test --all

  # ── 2. Cross-compile binaries ──────────────────────────────────────────────
  build:
    name: Build ${{ matrix.target }}
    needs: test
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      id-token: write       # for artifact attestation
      attestations: write   # for artifact attestation
    strategy:
      fail-fast: false
      matrix:
        include:
          # Linux x86_64 (musl = fully static, runs on any glibc-less distro)
          - target: x86_64-unknown-linux-musl
            runner: ubuntu-latest
            artifact: nanodns
            archive: nanodns-linux-x86_64.tar.gz

          # Linux aarch64 (musl static — Raspberry Pi, ARM servers)
          - target: aarch64-unknown-linux-musl
            runner: ubuntu-latest
            artifact: nanodns
            archive: nanodns-linux-aarch64.tar.gz

          # Linux armv7 (musl static — Raspberry Pi 32-bit)
          - target: armv7-unknown-linux-musleabihf
            runner: ubuntu-latest
            artifact: nanodns
            archive: nanodns-linux-armv7.tar.gz

          # macOS x86_64
          - target: x86_64-apple-darwin
            runner: macos-latest
            artifact: nanodns
            archive: nanodns-macos-x86_64.tar.gz

          # macOS Apple Silicon
          - target: aarch64-apple-darwin
            runner: macos-latest
            artifact: nanodns
            archive: nanodns-macos-aarch64.tar.gz

          # Windows x86_64
          - target: x86_64-pc-windows-msvc
            runner: windows-latest
            artifact: nanodns.exe
            archive: nanodns-windows-x86_64.zip

    steps:
      - uses: actions/checkout@v6

      - name: Install Rust stable + target
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      # cross handles musl cross-compilation transparently via Docker
      - name: Install cross (Linux musl targets)
        if: contains(matrix.target, 'musl')
        uses: taiki-e/install-action@v2
        with:
          tool: cross

      - name: Cache cargo
        uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-build-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-cargo-build-${{ matrix.target }}-

      # Inject version from tag into Cargo.toml at build time (no file edit)
      - name: Set version from tag
        shell: bash
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "RELEASE_VERSION=$VERSION" >> "$GITHUB_ENV"
          # Patch Cargo.toml version so --version matches the tag
          sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml

      - name: Build (cross — musl)
        if: contains(matrix.target, 'musl')
        run: cross build --release --target ${{ matrix.target }}

      - name: Build (cargo — native)
        if: "!contains(matrix.target, 'musl')"
        run: cargo build --release --target ${{ matrix.target }}

      # ── Package ─────────────────────────────────────────────────────────────
      # nanodns.json is NOT committed to the repo.
      # Cross-compiled binaries cannot run on the host runner, so we generate
      # the example config inline with Python (always available on GH runners).
      - name: Generate example config
        shell: python3 {0}
        run: |
          import json, pathlib
          cfg = {
            "server": {
              "host": "0.0.0.0", "port": 53,
              "upstream": ["8.8.8.8", "1.1.1.1"],
              "cache_enabled": True, "cache_ttl": 300, "cache_size": 1000,
              "log_level": "INFO", "log_queries": False,
              "hot_reload": True, "mgmt_port": 9053, "peers": []
            },
            "zones": {
              "internal.lan": {
                "soa": {
                  "mname": "ns1.internal.lan", "rname": "admin.internal.lan",
                  "serial": 2024010101, "refresh": 3600, "retry": 900,
                  "expire": 604800, "minimum": 300
                },
                "ns": ["ns1.internal.lan"]
              }
            },
            "records": [
              {"name": "web.internal.lan",   "type": "A",     "value": "192.168.1.100", "ttl": 300},
              {"name": "db.internal.lan",    "type": "A",     "value": "192.168.1.101"},
              {"name": "api.internal.lan",   "type": "CNAME", "value": "web.internal.lan"},
              {"name": "internal.lan",       "type": "MX",    "value": "mail.internal.lan", "priority": 10},
              {"name": "*.app.internal.lan", "type": "A",     "value": "192.168.1.200", "wildcard": True}
            ],
            "rewrites": [
              {"match": "ads.example.com", "action": "nxdomain"},
              {"match": "*.tracker.net",   "action": "nxdomain"}
            ],
            "version": 0
          }
          pathlib.Path("nanodns.example.json").write_text(
            json.dumps(cfg, indent=2), encoding="utf-8"
          )
          print("Generated nanodns.example.json")

      - name: Package (tar.gz — Unix)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          BIN=target/${{ matrix.target }}/release/${{ matrix.artifact }}
          strip "$BIN" 2>/dev/null || true
          # Collect everything into a staging dir so tar paths are predictable
          mkdir -p staging
          cp "$BIN"                staging/nanodns
          cp README.md             staging/README.md
          cp nanodns.example.json  staging/nanodns.example.json
          cp nanodns.service       staging/nanodns.service
          tar czf "${{ matrix.archive }}" -C staging .
          rm -rf staging

      - name: Package (zip — Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $bin = "target\${{ matrix.target }}\release\${{ matrix.artifact }}"
          Compress-Archive `
            -Path $bin, "README.md", "nanodns.example.json" `
            -DestinationPath "${{ matrix.archive }}"

      # ── Checksum ─────────────────────────────────────────────────────────────
      - name: SHA-256 checksum (Unix)
        if: runner.os != 'Windows'
        run: sha256sum "${{ matrix.archive }}" > "${{ matrix.archive }}.sha256"

      - name: SHA-256 checksum (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $hash = (Get-FileHash "${{ matrix.archive }}" -Algorithm SHA256).Hash.ToLower()
          "$hash  ${{ matrix.archive }}" | Out-File "${{ matrix.archive }}.sha256" -Encoding utf8

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@v2
        with:
          subject-path: ${{ matrix.archive }}

      - name: Upload artifact
        uses: actions/upload-artifact@v6
        with:
          name: ${{ matrix.archive }}
          path: |
            ${{ matrix.archive }}
            ${{ matrix.archive }}.sha256
          retention-days: 1

  # ── 3. Docker multi-arch image → GHCR ────────────────────────────────────
  docker:
    name: Docker (GHCR)
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write        # push to GHCR
      id-token: write        # cosign keyless signing + attestation
      attestations: write    # image attestation
    steps:
      - uses: actions/checkout@v6

      - name: Set up QEMU (for ARM builds)
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract Docker metadata (tags + labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            # vX.Y.Z  →  tag + major.minor + major + latest
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
            # short commit sha for traceability
            type=sha,prefix=sha-,format=short

      - name: Build and push multi-arch image
        id: docker-build
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ github.ref_name }}

      # Keyless signing with cosign (Sigstore) — verifiable supply chain
      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign image with cosign (keyless)
        env:
          DIGEST: ${{ steps.docker-build.outputs.digest }}
        run: |
          cosign sign --yes \
            "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}"

      - name: Generate Docker image attestation
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          subject-digest: ${{ steps.docker-build.outputs.digest }}
          push-to-registry: true

      - name: Output image digest
        run: echo "Image digest ${{ steps.docker-build.outputs.digest }}"

  # ── 4. Changelog  (parallel with build + docker, needs test gate only) ──────
  changelog:
    name: Generate Changelog
    runs-on: ubuntu-latest
    needs: test
    permissions:
      contents: read

    outputs:
      previous-tag: ${{ steps.tags.outputs.previous }}

    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Resolve tag range
        id: tags
        run: |
          CURRENT="${GITHUB_REF_NAME}"
          PREVIOUS=$(git tag --sort=-version:refname \
                     | grep -E '^v[0-9]' \
                     | sed -n '2p')
          echo "current=${CURRENT}"   >> "$GITHUB_OUTPUT"
          echo "previous=${PREVIOUS}" >> "$GITHUB_OUTPUT"
          echo "Current : ${CURRENT}"
          echo "Previous: ${PREVIOUS:-<first release>}"

      - name: Build author map (batch API fetch)
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO:     ${{ github.repository }}
          PREV:     ${{ steps.tags.outputs.previous }}
        run: |
          RANGE="${PREV:+${PREV}..}HEAD"
          : > /tmp/authors.tsv
          git log ${RANGE} --format="%H %h" | while IFS=' ' read -r FULL SHORT; do
            LOGIN=$(gh api \
              -H "Accept: application/vnd.github+json" \
              -H "X-GitHub-Api-Version: 2022-11-28" \
              "/repos/${REPO}/commits/${FULL}" \
              --jq '.author.login // empty' 2>/dev/null || true)
            [ -n "$LOGIN" ] && printf '%s\t@%s\n' "$SHORT" "$LOGIN" >> /tmp/authors.tsv
          done
          echo "Author map:"
          cat /tmp/authors.tsv || true

      - name: Write CHANGELOG.md
        env:
          CURRENT:  ${{ steps.tags.outputs.current }}
          PREVIOUS: ${{ steps.tags.outputs.previous }}
          REPO:     ${{ github.repository }}
        run: |
          RANGE="${PREVIOUS:+${PREVIOUS}..}HEAD"
          DATE=$(date +'%Y-%m-%d')
          mkdir -p changelog

          annotate() {
            while IFS= read -r line; do
              HASH=$(echo "$line" | grep -oE '\([a-f0-9]{7}\)' | tr -d '()')
              AUTHOR=$(grep "^${HASH}" /tmp/authors.tsv 2>/dev/null | cut -f2 || true)
              echo "$line${AUTHOR:+ ${AUTHOR}}"
            done
          }

          {
            echo "# Changelog"
            echo
            echo "## ${CURRENT} (${DATE})"
            echo

            FEATS=$(git log ${RANGE} --pretty=format:"- %s (%h)" --reverse \
                    | grep -E '^- feat(\([^)]+\))?:' \
                    | sed -E 's/^- feat(\([^)]+\))?:/- /' | annotate)
            [ -n "$FEATS" ] && { echo "### ⭐ New Features"; echo "$FEATS"; echo; }

            PERFS=$(git log ${RANGE} --pretty=format:"- %s (%h)" --reverse \
                    | grep -E '^- (perf|refactor)(\([^)]+\))?:' \
                    | sed -E 's/^- (perf|refactor)(\([^)]+\))?:/- /' | annotate)
            [ -n "$PERFS" ] && { echo "### ⚡️ Optimizations"; echo "$PERFS"; echo; }

            FIXES=$(git log ${RANGE} --pretty=format:"- %s (%h)" --reverse \
                    | grep -E '^- fix(\([^)]+\))?:' \
                    | sed -E 's/^- fix(\([^)]+\))?:/- /' | annotate)
            [ -n "$FIXES" ] && { echo "### 🐞 Bug Fixes"; echo "$FIXES"; echo; }

            DOCS=$(git log ${RANGE} --pretty=format:"- %s (%h)" --reverse \
                   | grep -E '^- docs(\([^)]+\))?:' \
                   | sed -E 's/^- docs(\([^)]+\))?:/- /' | annotate)
            [ -n "$DOCS" ] && { echo "### 📚 Documentation"; echo "$DOCS"; echo; }

            DEPS=$(git log ${RANGE} --pretty=format:"- %s (%h)" --reverse \
                   | grep -E '^- (build|deps)(\([^)]+\))?:' \
                   | sed -E 's/^- (build|deps)(\([^)]+\))?:/- /' | annotate)
            [ -n "$DEPS" ] && { echo "### ⬆️ Dependency Updates"; echo "$DEPS"; echo; }

            OTHERS=$(git log ${RANGE} --pretty=format:"- %s (%h)" --reverse \
                     | grep -vE '^- (feat|fix|docs|refactor|perf|build|deps)(\([^)]+\))?:' \
                     | annotate)
            [ -n "$OTHERS" ] && { echo "### 🔨 Other Changes"; echo "$OTHERS"; echo; }

            CONTRIBUTORS=$(cut -f2 /tmp/authors.tsv 2>/dev/null | sort -u | tr '\n' ' ')
            [ -n "$CONTRIBUTORS" ] && { echo "### 👥 Contributors"; echo "$CONTRIBUTORS"; echo; }

            [ -n "$PREVIOUS" ] && \
              echo "[Full Changelog](https://github.com/${REPO}/compare/${PREVIOUS}...${CURRENT})"
          } > changelog/CHANGELOG.md

          echo "--- CHANGELOG.md ---"
          cat changelog/CHANGELOG.md

      - uses: actions/upload-artifact@v6
        with:
          name: changelog
          path: changelog/CHANGELOG.md
          retention-days: 3

  # ── 5. Publish to crates.io (OIDC Trusted Publishing) ────────────────────────
  #
  # Uses rust-lang/crates-io-auth-action which exchanges GitHub's OIDC token
  # for a 30-minute crates.io access token — no stored secret needed.
  #
  # One-time setup on crates.io:
  #   1. Account Settings → Trusted Publishing → Add Publisher
  #      Repository : <your-org>/<your-repo>
  #      Workflow   : release.yml
  #      Environment: release          ← must match `environment.name` below
  #   2. Optionally enable "Require trusted publishing for all new versions"
  #
  # One-time setup in GitHub:
  #   Settings → Environments → New environment → name it "release"
  #
  # Version workflow: bump Cargo.toml version BEFORE pushing the tag.
  # The tag and Cargo.toml version must match — do NOT auto-patch here,
  # that leaves a Cargo.toml.bak dirty file that blocks `cargo publish`.
  publish-crates:
    name: Publish to crates.io
    needs: test                      # gate on tests; runs parallel with build/docker
    runs-on: ubuntu-latest
    # crates.io has no pre-release channel — skip tags like v1.2.3-rc1
    if: "!contains(github.ref_name, '-')"

    permissions:
      id-token: write                # required for OIDC token exchange + attestation
      contents: read
      attestations: write            # required for artifact attestation

    environment:
      name: release                  # matches Trusted Publishing environment on crates.io
      url: https://crates.io/crates/nanodns

    steps:
      - uses: actions/checkout@v6

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

      - name: Cache cargo registry
        uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-publish-${{ hashFiles('**/Cargo.lock') }}

      - name: Verify Cargo.toml version matches tag
        shell: bash
        run: |
          TAG_VERSION="${GITHUB_REF_NAME#v}"
          CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
          echo "Tag version   : ${TAG_VERSION}"
          echo "Cargo version : ${CARGO_VERSION}"
          if [ "${TAG_VERSION}" != "${CARGO_VERSION}" ]; then
            echo "::error::Version mismatch — Cargo.toml has ${CARGO_VERSION} but tag is ${TAG_VERSION}"
            echo "::error::Please bump Cargo.toml version before pushing the tag."
            exit 1
          fi

      - name: Package crate
        run: cargo package --no-verify

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@v2
        with:
          subject-path: target/package/*.crate

      - name: Authenticate with crates.io (OIDC)
        uses: rust-lang/crates-io-auth-action@v1
        id: auth

      - name: Publish to crates.io
        run: cargo publish --no-verify
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
        # --no-verify skips a local rebuild; tests already ran in the `test` job.

  # ── 6. GitHub Release + upload all binaries ───────────────────────────────
  github-release:
    name: GitHub Release
    needs: [build, docker, changelog, publish-crates]
    if: always() && !contains(needs.*.result, 'failure')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Download binary artifacts
        uses: actions/download-artifact@v6
        with:
          path: dist
          merge-multiple: true
          pattern: "nanodns-*"   # only binary archives, not the changelog artifact

      - name: Download changelog artifact
        uses: actions/download-artifact@v6
        with:
          name: changelog
          path: changelog

      - name: List release assets
        run: ls -lh dist/

      - name: Merge checksums
        run: |
          cat dist/*.sha256 | sort > dist/CHECKSUMS.txt
          echo "=== CHECKSUMS.txt ==="
          cat dist/CHECKSUMS.txt

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          name: "NanoDNS ${{ github.ref_name }}"
          body_path: changelog/CHANGELOG.md
          draft: false
          prerelease: ${{ contains(github.ref_name, '-') }}
          files: |
            dist/*.tar.gz
            dist/*.zip
            dist/CHECKSUMS.txt
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Annotate release with Docker info
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          TAG: ${{ github.ref_name }}
        run: |
          EXISTING=$(gh release view "$TAG" --json body -q .body)
          # Write annotation to a temp file to avoid YAML --- ambiguity
          cat > /tmp/docker_note.md << 'DOCKERNOTE'

          ---
          **Docker image** (multi-arch: amd64 / arm64 )
          ```
          docker pull IMAGE_PLACEHOLDER:TAG_PLACEHOLDER
          ```
          Verify signature:
          ```
          cosign verify \
            --certificate-identity-regexp='https://github.com/REPO_PLACEHOLDER/.github/workflows/release.yml@refs/tags/.*' \
            --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
            IMAGE_PLACEHOLDER:TAG_PLACEHOLDER
          ```
          DOCKERNOTE
          REPO="${{ github.repository }}"
          sed -i "s|IMAGE_PLACEHOLDER|${IMAGE}|g; s|TAG_PLACEHOLDER|${TAG#v}|g; s|REPO_PLACEHOLDER|${REPO}|g" /tmp/docker_note.md
          NOTES="${EXISTING}$(cat /tmp/docker_note.md)"
          gh release edit "$TAG" --notes "$NOTES" 2>/dev/null || true
