name: OpenAPI Gate - Breaking Change Detection

on:
  pull_request:
    branches:
      - main
    paths:
      - 'src/KArtSell.Host/Features/**/*.cs'
      - 'src/KArtSell.Modules.*/**/*.cs'
      - '.gitea/workflows/openapi-gate.yml'

jobs:
  openapi-diff:
    name: Detect Breaking Changes in OpenAPI Spec
    runs-on: ubuntu-latest

    steps:
      - name: Checkout PR branch
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

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

      - name: Restore dependencies
        run: dotnet restore

      - name: Build solution
        run: dotnet build -c Release --no-restore

      - name: Generate current OpenAPI spec
        run: |
          mkdir -p /tmp/openapi
          dotnet run --project src/KArtSell.Host -c Release -- \
            --generate-openapi-spec-only \
            --output /tmp/openapi/current.json || true

      - name: Checkout main branch
        run: |
          git fetch origin main:main
          git checkout main

      - name: Build main branch
        run: |
          dotnet restore
          dotnet build -c Release --no-restore

      - name: Generate baseline OpenAPI spec
        run: |
          dotnet run --project src/KArtSell.Host -c Release -- \
            --generate-openapi-spec-only \
            --output /tmp/openapi/baseline.json || true

      - name: Checkout PR branch again
        run: git checkout -

      - name: Analyze OpenAPI diff
        run: |
          # Compare specs and detect breaking changes
          python3 << 'EOF'
          import json
          import sys

          def load_spec(path):
              try:
                  with open(path) as f:
                      return json.load(f)
              except:
                  return {}

          baseline = load_spec('/tmp/openapi/baseline.json')
          current = load_spec('/tmp/openapi/current.json')

          breaking_changes = []

          # Check 1: Required parameter removed
          for path, baseline_ops in baseline.get('paths', {}).items():
              for method, baseline_op in baseline_ops.items():
                  if isinstance(baseline_op, dict):
                      baseline_params = {p['name']: p.get('required', False)
                                        for p in baseline_op.get('parameters', [])}

                      current_ops = current.get('paths', {}).get(path, {})
                      current_op = current_ops.get(method, {})
                      current_params = {p['name']: p.get('required', False)
                                       for p in current_op.get('parameters', [])}

                      for param_name, was_required in baseline_params.items():
                          if was_required and param_name not in current_params:
                              breaking_changes.append(
                                  f"BREAKING: Required parameter '{param_name}' removed from {method.upper()} {path}"
                              )

          # Check 2: Response status code removed
          for path, baseline_ops in baseline.get('paths', {}).items():
              for method, baseline_op in baseline_ops.items():
                  if isinstance(baseline_op, dict):
                      baseline_statuses = set(baseline_op.get('responses', {}).keys())

                      current_ops = current.get('paths', {}).get(path, {})
                      current_op = current_ops.get(method, {})
                      current_statuses = set(current_op.get('responses', {}).keys())

                      for status in ['200', '201', '202', '204']:
                          if status in baseline_statuses and status not in current_statuses:
                              breaking_changes.append(
                                  f"BREAKING: Response status {status} removed from {method.upper()} {path}"
                              )

          # Check 3: Required field removed from response
          for path, baseline_ops in baseline.get('paths', {}).items():
              for method, baseline_op in baseline_ops.items():
                  if isinstance(baseline_op, dict):
                      baseline_schema = baseline_op.get('responses', {}).get('200', {}).get('schema', {})
                      required_fields = set(baseline_schema.get('required', []))

                      current_ops = current.get('paths', {}).get(path, {})
                      current_op = current_ops.get(method, {})
                      current_schema = current_op.get('responses', {}).get('200', {}).get('schema', {})
                      current_fields = set(current_schema.get('properties', {}).keys())

                      for field in required_fields:
                          if field not in current_fields:
                              breaking_changes.append(
                                  f"BREAKING: Required field '{field}' removed from response of {method.upper()} {path}"
                              )

          if breaking_changes:
              print("❌ BREAKING CHANGES DETECTED:\n")
              for change in breaking_changes:
                  print(f"  - {change}")
              print("\n⛔ WORKFLOW HALTED: Cannot merge without approval\n")
              sys.exit(1)
          else:
              print("✅ No breaking changes detected in OpenAPI spec")
              sys.exit(0)
          EOF

      - name: Comment on PR (Breaking Changes)
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `⛔ **OpenAPI Gate Failed: Breaking Changes Detected**

This PR introduces breaking changes to the API contract:
- Required parameters removed
- Response fields removed
- Status codes removed

**Action Required:**
1. Modify your changes to be backward-compatible, OR
2. Request approval from @api-architects with justification

Breaking change approval requires:
- [x] Documented rationale (why breaking is necessary)
- [x] Migration plan for existing clients
- [x] Version bump (major version for breaking changes)`
            })

      - name: Comment on PR (All Clear)
        if: success()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `✅ **OpenAPI Gate Passed: No Breaking Changes**

Your API changes are backward-compatible. Safe to merge.`
            })

  openapi-approval:
    name: Manual Approval Gate (if breaking changes)
    if: failure()
    needs: openapi-diff
    runs-on: ubuntu-latest

    steps:
      - name: Require manual approval
        run: |
          echo "❌ Breaking changes detected. Waiting for @api-architects approval..."
          echo "GitHub PR Review required from 'api-architects' team before merging."
          exit 1

  openapi-specs-update:
    name: Update Committed OpenAPI Specs (if merged)
    if: success()
    needs: openapi-diff
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

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

      - name: Generate OpenAPI spec
        run: |
          mkdir -p docs/api
          dotnet run --project src/KArtSell.Host -c Release -- \
            --generate-openapi-spec-only \
            --output docs/api/openapi.json

      - name: Commit updated spec
        run: |
          git config user.email "ci@example.com"
          git config user.name "CI Bot"

          if ! git diff --quiet docs/api/openapi.json; then
            git add docs/api/openapi.json
            git commit -m "ci: Update OpenAPI specification (auto-generated)"
            git push
          fi
