name: 'sloc-guard'
description: 'Enforce source lines of code limits and directory structure constraints'
author: 'sloc-guard'
branding:
  icon: 'file-text'
  color: 'blue'

inputs:
  paths:
    description: 'Paths to check (space-separated). Defaults to current directory'
    required: false
    default: '.'
  config-path:
    description: 'Path to sloc-guard.toml configuration file'
    required: false
    default: ''
  fail-on-warning:
    description: 'Treat warnings as failures (exit code 1)'
    required: false
    default: 'false'
  version:
    description: 'sloc-guard version to install (e.g., "0.1.0"). Use "latest" for HEAD'
    required: false
    default: 'latest'
  cache:
    description: 'Enable caching of sloc-guard results (.git/sloc-guard/ or .sloc-guard/)'
    required: false
    default: 'true'
  sarif-output:
    description: 'Path for SARIF output file. Empty to disable'
    required: false
    default: ''
  baseline:
    description: 'Path to baseline file for grandfathering existing violations'
    required: false
    default: ''
  diff:
    description: 'Only check files changed since ref (e.g., "HEAD", "origin/main")'
    required: false
    default: ''

outputs:
  total-files:
    description: 'Total number of files checked'
    value: ${{ steps.run-check.outputs.total-files }}
  passed:
    description: 'Number of files that passed'
    value: ${{ steps.run-check.outputs.passed }}
  failed:
    description: 'Number of files that failed'
    value: ${{ steps.run-check.outputs.failed }}
  warnings:
    description: 'Number of files with warnings'
    value: ${{ steps.run-check.outputs.warnings }}
  grandfathered:
    description: 'Number of grandfathered violations (from baseline)'
    value: ${{ steps.run-check.outputs.grandfathered }}
  sarif-file:
    description: 'Path to generated SARIF file (if sarif-output was specified)'
    value: ${{ steps.run-check.outputs.sarif-file }}

runs:
  using: 'composite'
  steps:
    - name: Setup Rust
      uses: dtolnay/rust-toolchain@stable

    # Note: Cargo registry cache is only needed when falling back to `cargo install`.
    # Use restore-only here; save step runs conditionally after install.
    - name: Restore cargo registry cache
      id: cargo-cache
      uses: actions/cache/restore@v4
      with:
        path: |
          ~/.cargo/registry
          ~/.cargo/git
        key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-cargo-registry-

    - name: Detect platform and resolve version
      id: detect-platform
      shell: bash
      env:
        ACTION_REPO: ${{ github.action_repository }}
        VERSION: ${{ inputs.version }}
      run: |
        # Map runner OS to release platform name
        case "${{ runner.os }}" in
          Linux)   PLATFORM="linux" ;;
          macOS)   PLATFORM="macos" ;;
          Windows) PLATFORM="windows" ;;
          *)       PLATFORM="" ;;
        esac

        # Map runner arch to release arch name
        case "${{ runner.arch }}" in
          X64)   ARCH="x64" ;;
          ARM64) ARCH="arm64" ;;
          *)     ARCH="" ;;
        esac

        # Resolve version for "latest" to enable proper cache invalidation
        # Note: GitHub API has rate limits for unauthenticated requests (60/hour).
        # In high-traffic scenarios, this may fail and fall back to cargo install.
        RESOLVED_VERSION="$VERSION"
        if [ "$VERSION" = "latest" ]; then
          echo "Resolving latest version..."
          LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/${ACTION_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/' || echo "")
          if [ -n "$LATEST_TAG" ]; then
            RESOLVED_VERSION="$LATEST_TAG"
            echo "Latest version: $RESOLVED_VERSION"
          else
            echo "Could not determine latest version from GitHub API, will fall back to cargo install"
            RESOLVED_VERSION="latest-fallback"
          fi
        elif [[ "$VERSION" != v* ]]; then
          # Ensure version has 'v' prefix for consistency
          RESOLVED_VERSION="v$VERSION"
        fi

        echo "platform=$PLATFORM" >> $GITHUB_OUTPUT
        echo "arch=$ARCH" >> $GITHUB_OUTPUT
        echo "resolved_version=$RESOLVED_VERSION" >> $GITHUB_OUTPUT

    - name: Cache sloc-guard binary
      id: cache-binary
      uses: actions/cache@v4
      with:
        path: ~/.cargo/bin/sloc-guard*
        key: sloc-guard-${{ steps.detect-platform.outputs.platform }}-${{ steps.detect-platform.outputs.arch }}-${{ steps.detect-platform.outputs.resolved_version }}

    - name: Install sloc-guard
      id: install
      if: steps.cache-binary.outputs.cache-hit != 'true'
      shell: bash
      env:
        ACTION_REPO: ${{ github.action_repository }}
        PLATFORM: ${{ steps.detect-platform.outputs.platform }}
        ARCH: ${{ steps.detect-platform.outputs.arch }}
        VERSION: ${{ inputs.version }}
        RESOLVED_VERSION: ${{ steps.detect-platform.outputs.resolved_version }}
      run: |
        REPO_URL="https://github.com/${ACTION_REPO}"
        INSTALL_DIR="$HOME/.cargo/bin"
        mkdir -p "$INSTALL_DIR"

        # Function: download with exponential backoff retry
        download_with_retry() {
          local url="$1" output="$2" max_retries=3 delay=2
          for ((i=1; i<=max_retries; i++)); do
            echo "Attempt $i: Downloading $url"
            if curl -fsSL "$url" -o "$output" 2>/dev/null; then
              return 0
            fi
            if [ $i -lt $max_retries ]; then
              echo "Download failed, retrying in ${delay}s..."
              sleep $delay
              delay=$((delay * 2))
            fi
          done
          return 1
        }

        # Function: verify SHA256 checksum
        verify_checksum() {
          local file="$1" sums_file="$2" filename
          filename=$(basename "$file")
          expected=$(grep "$filename" "$sums_file" | cut -d' ' -f1)
          if [ -z "$expected" ]; then
            echo "Checksum not found for $filename"
            return 1
          fi
          echo "Verifying checksum for $filename..."
          if [ "${{ runner.os }}" = "Windows" ]; then
            actual=$(sha256sum "$file" | cut -d' ' -f1)
            if [ "$expected" = "$actual" ]; then
              return 0
            else
              echo "Checksum mismatch: expected $expected, got $actual"
              return 1
            fi
          else
            echo "$expected  $file" | sha256sum -c -
          fi
        }

        # Function: fallback to cargo install
        fallback_cargo_install() {
          echo "Falling back to cargo install..."
          echo "used-cargo-install=true" >> $GITHUB_OUTPUT
          if [ "$VERSION" = "latest" ]; then
            cargo install --git "$REPO_URL" --locked
          else
            cargo install --git "$REPO_URL" --tag "v$VERSION" --locked
          fi
        }

        # Skip binary download if platform/arch is unknown or version resolution failed
        if [ -z "$PLATFORM" ] || [ -z "$ARCH" ]; then
          echo "Unknown platform/arch: ${{ runner.os }}-${{ runner.arch }}"
          fallback_cargo_install
          exit 0
        fi

        # Use pre-resolved version from detect-platform step
        if [ "$RESOLVED_VERSION" = "latest-fallback" ]; then
          echo "Version resolution failed earlier"
          fallback_cargo_install
          exit 0
        fi

        # Determine archive extension
        if [ "$PLATFORM" = "windows" ]; then
          ARCHIVE_EXT="zip"
        else
          ARCHIVE_EXT="tar.gz"
        fi

        # Construct download URLs matching release.yml format:
        # sloc-guard-${VERSION}-${PLATFORM}-${ARCH}.${EXT}
        ARCHIVE_NAME="sloc-guard-${RESOLVED_VERSION}-${PLATFORM}-${ARCH}.${ARCHIVE_EXT}"
        ARCHIVE_URL="${REPO_URL}/releases/download/${RESOLVED_VERSION}/${ARCHIVE_NAME}"
        SUMS_URL="${REPO_URL}/releases/download/${RESOLVED_VERSION}/checksums-sha256.txt"

        # Create temp directory
        TEMP_DIR=$(mktemp -d)
        trap "rm -rf '$TEMP_DIR'" EXIT

        # Download binary archive
        ARCHIVE_PATH="$TEMP_DIR/$ARCHIVE_NAME"
        if ! download_with_retry "$ARCHIVE_URL" "$ARCHIVE_PATH"; then
          echo "Binary download failed"
          fallback_cargo_install
          exit 0
        fi

        # Download checksums
        SUMS_PATH="$TEMP_DIR/checksums-sha256.txt"
        if ! download_with_retry "$SUMS_URL" "$SUMS_PATH"; then
          echo "Checksum file download failed"
          fallback_cargo_install
          exit 0
        fi

        # Verify checksum
        if ! verify_checksum "$ARCHIVE_PATH" "$SUMS_PATH"; then
          echo "Checksum verification failed"
          fallback_cargo_install
          exit 0
        fi

        # Extract archive
        echo "Extracting binary..."
        if [ "$PLATFORM" = "windows" ]; then
          unzip -q "$ARCHIVE_PATH" -d "$TEMP_DIR/extracted"
        else
          mkdir -p "$TEMP_DIR/extracted"
          tar -xzf "$ARCHIVE_PATH" -C "$TEMP_DIR/extracted"
        fi

        # Find and install binary
        if [ "$PLATFORM" = "windows" ]; then
          BINARY=$(find "$TEMP_DIR/extracted" -name "sloc-guard.exe" -type f | head -1)
        else
          BINARY=$(find "$TEMP_DIR/extracted" -name "sloc-guard" -type f | head -1)
        fi

        if [ -z "$BINARY" ] || [ ! -f "$BINARY" ]; then
          echo "Binary not found in archive"
          fallback_cargo_install
          exit 0
        fi

        cp "$BINARY" "$INSTALL_DIR/"
        chmod +x "$INSTALL_DIR/sloc-guard"* 2>/dev/null || true
        echo "Successfully installed sloc-guard from pre-built binary"
        echo "used-cargo-install=false" >> $GITHUB_OUTPUT

    # Note: Save cargo registry cache only when cargo install was used AND cache wasn't restored.
    # - When binary cache hits, install step is skipped, so used-cargo-install is undefined (empty)
    # - When cargo registry cache hits, don't re-save identical content
    - name: Save cargo registry cache
      if: steps.install.outputs.used-cargo-install == 'true' && steps.cargo-cache.outputs.cache-hit != 'true'
      uses: actions/cache/save@v4
      with:
        path: |
          ~/.cargo/registry
          ~/.cargo/git
        key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}

    - name: Generate cache key
      id: cache-key
      shell: bash
      run: |
        CONFIG_PATH="${{ inputs.config-path }}"
        if [ -z "$CONFIG_PATH" ]; then
          CONFIG_PATH="sloc-guard.toml"
        fi
        if [ -f "$CONFIG_PATH" ]; then
          CONFIG_HASH=$(sha256sum "$CONFIG_PATH" | cut -d' ' -f1 | head -c 16)
        else
          CONFIG_HASH="default"
        fi
        echo "hash=$CONFIG_HASH" >> $GITHUB_OUTPUT

    # Note: sloc-guard stores cache in .git/sloc-guard/ (git repos) or .sloc-guard/ (non-git).
    # Uses default save behavior (save only on job success) which is intentional - failed
    # jobs may have incomplete/invalid cache state that shouldn't be persisted.
    - name: Cache sloc-guard results
      if: inputs.cache == 'true'
      uses: actions/cache@v4
      with:
        path: |
          .git/sloc-guard/cache.json
          .sloc-guard/cache.json
        key: sloc-guard-cache-${{ steps.cache-key.outputs.hash }}-${{ inputs.version }}-${{ github.sha }}
        restore-keys: |
          sloc-guard-cache-${{ steps.cache-key.outputs.hash }}-${{ inputs.version }}-

    - name: Register problem matcher
      shell: bash
      run: echo "::add-matcher::${{ github.action_path }}/problem-matcher.json"

    - name: Run sloc-guard check
      id: run-check
      shell: bash
      env:
        INPUT_CONFIG_PATH: ${{ inputs.config-path }}
        INPUT_FAIL_ON_WARNING: ${{ inputs.fail-on-warning }}
        INPUT_SARIF_OUTPUT: ${{ inputs.sarif-output }}
        INPUT_BASELINE: ${{ inputs.baseline }}
        INPUT_DIFF: ${{ inputs.diff }}
        INPUT_PATHS: ${{ inputs.paths }}
        RUNNER_TEMP: ${{ runner.temp }}
      run: |
        # Build command as array for proper quoting and shell safety
        CMD=(sloc-guard check)

        # Config path
        if [ -n "$INPUT_CONFIG_PATH" ]; then
          CMD+=(--config "$INPUT_CONFIG_PATH")
        fi

        # Fail on warning (--strict treats warnings as errors)
        if [ "$INPUT_FAIL_ON_WARNING" = "true" ]; then
          CMD+=(--strict)
        fi

        # SARIF output (stored for --write-sarif, no --format sarif needed)
        SARIF_FILE=""
        if [ -n "$INPUT_SARIF_OUTPUT" ]; then
          SARIF_FILE="$INPUT_SARIF_OUTPUT"
        fi

        # Baseline
        if [ -n "$INPUT_BASELINE" ]; then
          CMD+=(--baseline "$INPUT_BASELINE")
        fi

        # Diff mode
        if [ -n "$INPUT_DIFF" ]; then
          CMD+=(--diff "$INPUT_DIFF")
        fi

        # Create temp file for JSON output (always needed for statistics)
        # Use --write-json for check results (has passed/failed/warnings/grandfathered)
        JSON_FILE="$RUNNER_TEMP/sloc-guard-output.json"
        CMD+=(--write-json "$JSON_FILE")
        
        # Add SARIF output if requested
        if [ -n "$SARIF_FILE" ]; then
          CMD+=(--write-sarif "$SARIF_FILE")
        fi
        
        # Force text output for problem matcher, disable color
        CMD+=(--format text --color never)
        
        # Add paths at the end (intentional word splitting for space-separated paths)
        # shellcheck disable=SC2206
        CMD+=($INPUT_PATHS)
        
        set +e
        TEXT_OUTPUT=$("${CMD[@]}" 2>&1)
        EXIT_CODE=$?
        set -e
        
        # Read JSON output from file
        if [ -f "$JSON_FILE" ]; then
          JSON_OUTPUT=$(cat "$JSON_FILE")
        else
          # JSON file missing - this may indicate a sloc-guard failure before output was written
          if [ "$EXIT_CODE" -ne 0 ] && [ "$EXIT_CODE" -ne 1 ]; then
            echo "::warning::JSON output file not created (exit code: $EXIT_CODE)"
          fi
          JSON_OUTPUT="{}"
        fi

        # Parse statistics from JSON output
        if command -v jq &> /dev/null; then
          TOTAL=$(echo "$JSON_OUTPUT" | jq -r '.summary.total_files // 0' 2>/dev/null || echo "0")
          PASSED=$(echo "$JSON_OUTPUT" | jq -r '.summary.passed // 0' 2>/dev/null || echo "0")
          FAILED=$(echo "$JSON_OUTPUT" | jq -r '.summary.failed // 0' 2>/dev/null || echo "0")
          WARNINGS=$(echo "$JSON_OUTPUT" | jq -r '.summary.warnings // 0' 2>/dev/null || echo "0")
          GRANDFATHERED=$(echo "$JSON_OUTPUT" | jq -r '.summary.grandfathered // 0' 2>/dev/null || echo "0")
        else
          TOTAL="0"
          PASSED="0"
          FAILED="0"
          WARNINGS="0"
          GRANDFATHERED="0"
        fi

        # Set outputs
        echo "total-files=$TOTAL" >> $GITHUB_OUTPUT
        echo "passed=$PASSED" >> $GITHUB_OUTPUT
        echo "failed=$FAILED" >> $GITHUB_OUTPUT
        echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
        echo "grandfathered=$GRANDFATHERED" >> $GITHUB_OUTPUT
        echo "sarif-file=$SARIF_FILE" >> $GITHUB_OUTPUT

        # Print text output for workflow logs (triggers problem matcher)
        echo "$TEXT_OUTPUT"

        # Exit with original code
        exit $EXIT_CODE

    - name: Generate Job Summary
      if: always()
      shell: bash
      env:
        TOTAL: ${{ steps.run-check.outputs.total-files }}
        PASSED: ${{ steps.run-check.outputs.passed }}
        FAILED: ${{ steps.run-check.outputs.failed }}
        WARNINGS: ${{ steps.run-check.outputs.warnings }}
        GRANDFATHERED: ${{ steps.run-check.outputs.grandfathered }}
        SARIF_FILE: ${{ steps.run-check.outputs.sarif-file }}
      run: |
        # Determine overall status
        if [ "$FAILED" = "0" ] && [ "$WARNINGS" = "0" ]; then
          STATUS_ICON="✅"
          STATUS_TEXT="All checks passed"
        elif [ "$FAILED" = "0" ]; then
          STATUS_ICON="⚠️"
          STATUS_TEXT="Passed with warnings"
        else
          STATUS_ICON="❌"
          STATUS_TEXT="Check failed"
        fi

        # Generate markdown summary
        {
          echo "## $STATUS_ICON sloc-guard Results"
          echo ""
          echo "**Status**: $STATUS_TEXT"
          echo ""
          echo "| Metric | Count |"
          echo "|--------|-------|"
          echo "| Total Files | $TOTAL |"
          echo "| ✓ Passed | $PASSED |"
          echo "| ⚠ Warnings | $WARNINGS |"
          echo "| ✗ Failed | $FAILED |"
          echo "| ◉ Grandfathered | $GRANDFATHERED |"
        } >> $GITHUB_STEP_SUMMARY

        # Add SARIF file info if generated
        if [ -n "$SARIF_FILE" ]; then
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "📄 SARIF output: \`$SARIF_FILE\`" >> $GITHUB_STEP_SUMMARY
        fi

    - name: Remove problem matcher
      if: always()
      shell: bash
      run: |
        echo "::remove-matcher owner=sloc-guard-error::"
        echo "::remove-matcher owner=sloc-guard-error-reason::"
        echo "::remove-matcher owner=sloc-guard-warning::"
        echo "::remove-matcher owner=sloc-guard-warning-reason::"
