name: CI

on:
  push:
    branches: [main, develop]
    tags: ['v*']
  pull_request:
    branches: [main, develop]
  workflow_dispatch:
    inputs:
      release_ref:
        description: Git ref to build, for example v1.8.4
        required: false
        type: string
      release_tag:
        description: Release image tag to publish, for example v1.8.4
        required: false
        type: string
      promote_stable:
        description: Also publish the stable image tag
        required: false
        default: false
        type: boolean
  schedule:
    # Weekly security scan on Mondays at 00:00 UTC
    - cron: '0 0 * * 1'

env:
  K8S_STATE_REPO: christianlouis/k8s-cluster-state

jobs:
  # ── Stage 1: Lint (gates everything else) ────────────────────────────────
  lint:
    name: Lint
    runs-on: ubuntu-latest
    permissions:
      contents: read

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

      - name: Set up Python 3.13
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'

      - name: Cache pip packages
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }}
          restore-keys: ${{ runner.os }}-pip-

      - name: Install linting tools
        run: |
          python -m pip install --upgrade pip
          pip install black isort flake8 pylint
          cd backend && pip install -r requirements.txt

      - name: Auto-format with Black and isort
        run: |
          black backend/app
          isort backend/app

      - name: Black – format check
        run: black --check backend/app

      - name: isort – import order check
        run: isort --check-only backend/app

      - name: Flake8
        run: flake8 backend/app

      - name: Pylint
        run: pylint backend/app
        continue-on-error: true

  # ── Stage 2 (parallel): Test, Security, CodeQL, Dependency Review ────────
  test:
    name: Test
    runs-on: ubuntu-latest
    needs: lint
    permissions:
      contents: read

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

      - name: Set up Python 3.13
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'

      - name: Cache pip packages
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }}
          restore-keys: ${{ runner.os }}-pip-

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          cd backend && pip install -r requirements.txt

      - name: Run tests with coverage
        run: |
          cd backend
          pytest --cov=app --cov-report=xml --cov-report=term-missing --cov-fail-under=80

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          file: ./backend/coverage.xml
          flags: unittests
          name: codecov-umbrella
          fail_ci_if_error: false

  security:
    name: Security Scan
    runs-on: ubuntu-latest
    needs: lint
    permissions:
      contents: read

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

      - name: Set up Python 3.13
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'

      - name: Cache pip packages
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }}
          restore-keys: ${{ runner.os }}-pip-

      - name: Install security tools
        run: |
          python -m pip install --upgrade pip
          pip install bandit pip-audit
          cd backend && pip install -r requirements.txt

      - name: Bandit – Python security linter
        run: bandit -r backend/app -f json -o bandit-report.json
        continue-on-error: true

      - name: Upload Bandit report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: bandit-security-report
          path: bandit-report.json

      - name: pip-audit – dependency vulnerability check
        run: pip-audit --requirement backend/requirements.txt
        continue-on-error: true

  codeql:
    name: CodeQL Analysis
    runs-on: ubuntu-latest
    needs: lint
    # CodeQL is only meaningful on real pushes/PRs, not the weekly schedule alone
    if: github.event_name != 'schedule'
    permissions:
      actions: read
      contents: read
      security-events: write

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

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: python
          queries: security-and-quality

      - name: Autobuild
        uses: github/codeql-action/autobuild@v3

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: /language:python

  dependency-review:
    name: Dependency Review
    runs-on: ubuntu-latest
    needs: lint
    if: github.event_name == 'pull_request'
    permissions:
      contents: read

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

      - name: Dependency Review
        uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: moderate

  # ── Stage 3: Docker Build & Publish (only on main push) ──────────────────
  docker:
    name: Docker Build & Publish
    runs-on: ubuntu-latest
    needs: [test, security]
    if: >-
      (github.event_name == 'push' && github.ref == 'refs/heads/main') ||
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) ||
      github.event_name == 'workflow_dispatch'
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.inputs.release_ref || github.ref }}

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

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=ref,event=tag
            type=semver,pattern={{version}}
            type=sha,prefix=
            type=raw,value=${{ github.event.inputs.release_tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '' }}
            type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') || github.event.inputs.promote_stable == 'true' }}
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: ./backend
          file: ./backend/Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ── Stage 4: GitOps – update preprod k8s manifest ────────────────────────
  update-k8s-manifest:
    name: Update Preprod K8s Manifest
    runs-on: ubuntu-latest
    needs: [docker]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    permissions:
      contents: read

    steps:
      - name: Compute image tag
        id: tag
        run: |
          SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
          echo "image=ghcr.io/${{ github.repository }}:${SHORT_SHA}" >> "$GITHUB_OUTPUT"
          echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
          echo "image_pattern=^ghcr\\.io/${{ github.repository }}:" >> "$GITHUB_OUTPUT"

      - name: Check if GH_PAT is configured and has repo access
        id: pat-check
        env:
          GH_PAT: ${{ secrets.GH_PAT }}
        run: |
          if [ -z "$GH_PAT" ]; then
            echo "::warning::GH_PAT secret is not configured. Skipping k8s manifest update."
            echo "available=false" >> "$GITHUB_OUTPUT"
          else
            HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
              -H "Authorization: Bearer $GH_PAT" \
              "https://api.github.com/repos/${{ env.K8S_STATE_REPO }}")
            if [ "$HTTP_CODE" = "200" ]; then
              echo "available=true" >> "$GITHUB_OUTPUT"
            else
              echo "::warning::GH_PAT does not have access to ${{ env.K8S_STATE_REPO }} (HTTP $HTTP_CODE). Skipping k8s manifest update."
              echo "available=false" >> "$GITHUB_OUTPUT"
            fi
          fi

      - name: Checkout k8s-cluster-state
        if: steps.pat-check.outputs.available == 'true'
        uses: actions/checkout@v4
        with:
          repository: ${{ env.K8S_STATE_REPO }}
          token: ${{ secrets.GH_PAT }}
          path: k8s-cluster-state
          ref: main

      - name: Update image tag in preprod manifest
        if: steps.pat-check.outputs.available == 'true'
        uses: mikefarah/yq@v4.44.6
        env:
          IMAGE: ${{ steps.tag.outputs.image }}
          IMAGE_PATTERN: ${{ steps.tag.outputs.image_pattern }}
        with:
          cmd: |
            yq -i '(.. | select(tag == "!!str") | select(test(strenv(IMAGE_PATTERN)))) = strenv(IMAGE)' \
              k8s-cluster-state/apps/dmarq/preprod/dmarq-stack.yaml

      - name: Commit and push manifest update
        if: steps.pat-check.outputs.available == 'true'
        run: |
          cd k8s-cluster-state
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add apps/dmarq/preprod/dmarq-stack.yaml
          if git diff --staged --quiet; then
            echo "No changes to commit"
          else
            git commit -m "chore(preprod): update dmarq image to ${{ steps.tag.outputs.short_sha }}"
            git push
          fi
