name: CI

# Triggers
#   push:main         full pipeline incl. cog auto-bump + publish + github-release
#   push:tags:["v*"]  release-only path; skip lint/test/build, run publish + github-release
#                     against the named tag
#   pull_request      lint/test/build/cargo-deny only — no release work
#   schedule          nightly cargo-deny at 04:00 UTC to surface fresh advisories
#   workflow_dispatch maintainer-named (re-)release; empty `tag` input runs the normal
#                     main pipeline

on:
  push:
    branches: [ main ]
    tags: [ "v*" ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 4 * * *'
  workflow_dispatch:
    inputs:
      tag:
        description: >-
          Existing tag to (re-)release (e.g. "v0.2.0"). Empty input
          runs the normal main pipeline.
        required: false
        type: string

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

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

jobs:
  # ----------------------------------------------------------------
  # Mode prelude. Every release-aware job's `if:` reads `release_tag`.
  # Non-empty when this run is a release-only path (tag push or
  # workflow_dispatch with `tag` input). Empty otherwise — the
  # normal "ran on main" / "PR" pipeline.
  # ----------------------------------------------------------------
  resolve:
    name: Resolve release tag
    runs-on: ubuntu-latest
    outputs:
      release_tag: ${{ steps.r.outputs.release_tag }}
    steps:
      - id: r
        run: |
          set -euo pipefail
          tag=""
          case "${{ github.event_name }}" in
            push)
              if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
                tag="${{ github.ref_name }}"
              fi
              ;;
            workflow_dispatch)
              tag='${{ inputs.tag }}'
              ;;
          esac
          echo "release_tag=${tag}"
          echo "release_tag=${tag}" >> "$GITHUB_OUTPUT"

  # Supply-chain enforcement. Runs on every PR + nightly cron @ 04:00 UTC.
  # Independent of the rest of the pipeline so a fresh advisory surfacing
  # nightly is visible without rerunning lint/test.
  cargo-deny:
    name: cargo-deny
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: EmbarkStudios/cargo-deny-action@v2
        with:
          command: check all

  commitlint:
    name: Commit lint (conventional-commits)
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: wagoid/commitlint-github-action@v6

  lint:
    name: Lint and Format Check
    runs-on: ubuntu-latest
    needs: [resolve]
    if: needs.resolve.outputs.release_tag == ''
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          components: rustfmt, clippy, cargo
      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-stable-lint-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-stable-lint-

      - name: Check code formatting
        run: cargo fmt --all -- --check

      - name: Run clippy (code smells check)
        run: |
          cargo clippy --version
          # Run clippy with practical code smell checks
          # Focus on catching critical issues: debug macros, unsafe patterns, etc.
          cargo clippy --all-targets --all-features -- -D warnings \
            -D clippy::dbg_macro \
            -D clippy::print_stdout \
            -D clippy::print_stderr \
            -D clippy::todo \
            -D clippy::unimplemented \
            -D clippy::panic \
            -D clippy::exit \
            -D clippy::cast_lossless \
            -D clippy::cast_possible_truncation \
            -D clippy::cast_possible_wrap \
            -D clippy::cast_precision_loss \
            -D clippy::cast_sign_loss \
            -D clippy::clone_on_ref_ptr \
            -D clippy::empty_enums \
            -D clippy::enum_glob_use \
            -D clippy::if_not_else \
            -D clippy::mut_mut \
            -D clippy::non_ascii_literal \
            -D clippy::single_match_else \
            -D clippy::string_add \
            -D clippy::string_add_assign \
            -D clippy::string_lit_as_bytes \
            -D clippy::unnecessary_unwrap \
            -D clippy::unused_self \
            -D clippy::useless_let_if_seq \
            -A clippy::module_name_repetitions \
            -A clippy::must_use_candidate \
            -A clippy::missing_errors_doc \
            -A clippy::missing_panics_doc \
            -A clippy::too_many_lines \
            -A clippy::similar_names \
            -A clippy::inline_always \
            -A clippy::unwrap_used \
            -A clippy::expect_used \
            -A clippy::panic \
            -A unused_imports \
            -A unused_macros \
            -A dead_code

      - name: Check documentation builds
        run: cargo doc --no-deps --all-features --document-private-items

  test:
    name: Test
    runs-on: ubuntu-latest
    needs: [resolve, lint]
    if: needs.resolve.outputs.release_tag == ''
    strategy:
      matrix:
        rust:
          - stable
          - beta
          - nightly
        include:
          - rust: nightly
            allow_failure: true
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          components: rustfmt, clippy, cargo
      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-${{ matrix.rust }}-

      - name: Check formatting
        run: cargo fmt --all -- --check
        continue-on-error: ${{ matrix.rust == 'nightly' }}

      - name: Run clippy
        run: |
          cargo clippy --version
          cargo clippy --all-targets --all-features -- -D warnings \
            -D clippy::dbg_macro \
            -D clippy::print_stdout \
            -D clippy::print_stderr \
            -A clippy::unwrap_used \
            -A clippy::expect_used \
            -A clippy::panic
        continue-on-error: ${{ matrix.rust == 'nightly' }}

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

      - name: Build
        run: cargo build --verbose --release --all-features

  build:
    name: Build
    runs-on: ${{ matrix.os }}
    needs: [resolve, lint, test]
    if: needs.resolve.outputs.release_tag == ''
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          components: rustfmt, clippy, cargo
          toolchain: ${{ matrix.rust }}
      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-stable-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-stable-
      - name: Build
        run: cargo build --verbose --release --all-features

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-${{ matrix.os }}
          path: target/release/
          if-no-files-found: ignore

  # ----------------------------------------------------------------
  # Version bump + tag. Runs after every check on `push: main` (PR
  # runs and tag-push runs skip). Cog inspects the conventional-
  # commit log since the last tag and creates a `chore(version):
  # vX.Y.Z` commit + matching tag if `feat/fix/breaking` commits
  # have landed.
  # ----------------------------------------------------------------
  tag:
    name: Bump version & tag
    runs-on: ubuntu-latest
    needs: [resolve, lint, test, build]
    if: |
      needs.resolve.outputs.release_tag == '' &&
      github.event_name == 'push' &&
      github.ref == 'refs/heads/main' &&
      !startsWith(github.event.head_commit.message, 'chore(version):') &&
      !contains(github.event.head_commit.message, '[skip ci]')
    outputs:
      tag: ${{ steps.bump.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Configure git identity
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Cocogitto bump
        uses: cocogitto/cocogitto-action@v4
        with:
          command: bump
          args: --auto
          git-user: "github-actions[bot]"
          git-user-email: "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Resolve created tag (if any)
        id: bump
        run: |
          set -euo pipefail
          tag="$(git tag --points-at HEAD | grep '^v' | head -n1 || true)"
          echo "tag=${tag}"
          echo "tag=${tag}" >> "$GITHUB_OUTPUT"

  # ----------------------------------------------------------------
  # Publish (cog flow). Fires on (a) a fresh cog-pushed tag in the
  # run above, or (b) a manual tag push / workflow_dispatch with
  # `tag` input. The "no new bump" case (tag job runs but creates no
  # tag) cleanly short-circuits via the `tag.outputs.tag != ''` guard.
  # ----------------------------------------------------------------
  publish:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    needs: [resolve, tag]
    if: |
      always() &&
      (
        (needs.tag.result == 'success' && needs.tag.outputs.tag != '') ||
        needs.resolve.outputs.release_tag != ''
      )
    steps:
      - uses: actions/checkout@v4
        with:
          # Check out the freshly-created tag (or the manually-named
          # one) so cargo publish ships the bumped Cargo.toml the
          # `tag` job's cog run wrote, not the pre-bump HEAD.
          ref: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }}
      - uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          components: cargo

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          cargo publish --verbose --token "${CARGO_REGISTRY_TOKEN}"

      # Re-create the .crate locally. `cargo publish` cleans up
      # target/package/*.crate after a successful upload; `cargo
      # package --no-verify` is fast (skips the re-build) and
      # deterministic given the same source tree, so the resulting
      # bytes match what crates.io just received.
      - name: Re-package .crate for artifact upload
        if: success()
        run: cargo package --no-verify

      # Upload the byte-identical `.crate` as a workflow artifact so
      # the `github-release` job can attach it to the GH Release
      # alongside the crates.io copy. Signing in a follow-up commit
      # will produce one sigstore signature that verifies both the
      # GH-Release-attached and crates.io copies.
      - name: Upload .crate as workflow artifact
        if: success()
        uses: actions/upload-artifact@v4
        with:
          name: crate-tarball
          path: target/package/*.crate
          if-no-files-found: error
          retention-days: 1

  # ----------------------------------------------------------------
  # GitHub release. Attaches the byte-identical `.crate` (from the
  # `publish` job's workflow artifact) and a SHA256SUMS file
  # alongside the auto-generated notes. The crates.io upload remains
  # the canonical install channel; the GH Release adds an
  # alternate-checksummed copy for downstream consumers that prefer
  # GitHub-hosted artifacts (and is the signing pipeline's anchor in
  # a follow-up commit).
  # ----------------------------------------------------------------
  github-release:
    name: Create GitHub release
    runs-on: ubuntu-latest
    needs: [resolve, tag, publish]
    if: |
      always() &&
      (
        (needs.tag.result == 'success' && needs.tag.outputs.tag != '') ||
        needs.resolve.outputs.release_tag != ''
      )
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }}

      - name: Download .crate artifact
        if: needs.publish.result == 'success'
        uses: actions/download-artifact@v4
        with:
          name: crate-tarball
          path: dist/

      - name: Generate SHA256SUMS
        if: needs.publish.result == 'success'
        run: |
          set -euo pipefail
          cd dist
          shasum -a 256 *.crate > SHA256SUMS
          echo "---"
          cat SHA256SUMS

      - name: Compose release body
        env:
          TAG: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }}
          PUBLISH_RESULT: ${{ needs.publish.result }}
        run: |
          set -euo pipefail
          {
            echo "## Install"
            echo
            echo "    cargo add pso-poseidon@${TAG#v}"
            echo
            echo "## Artifacts"
            echo
            echo "- \`pso-poseidon-${TAG#v}.crate\` — byte-identical to the crates.io upload."
            echo "- \`SHA256SUMS\` — SHA-256 of the above."
            echo
            echo "## CI"
            echo
            echo "- crates.io publish: ${PUBLISH_RESULT}"
          } > RELEASE_BODY.md
          cat RELEASE_BODY.md

      - name: Create GitHub release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }}
          name: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }}
          body_path: RELEASE_BODY.md
          generate_release_notes: true
          fail_on_unmatched_files: false
          files: |
            dist/*.crate
            dist/SHA256SUMS
