name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:
    inputs:
      ref:
        description: "Branch or ref to run CI against"
        required: false
        default: "main"

jobs:
  lint-typecheck:
    name: Lint & Typecheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '9.15.4'

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Typecheck
        run: pnpm typecheck

      - name: Lint
        run: pnpm lint

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '9.15.4'

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests
        run: pnpm test

  e2e:
    name: E2E Tests
    runs-on: ubuntu-latest
    needs: [lint-typecheck, test]
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '9.15.4'

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright browsers
        run: pnpm --filter @groombook/e2e exec playwright install --with-deps chromium

      - name: Start Docker Compose stack
        run: docker compose up -d --wait
        timeout-minutes: 5

      - name: Run E2E tests
        run: pnpm --filter @groombook/e2e test

      - name: Upload Playwright report
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: apps/e2e/playwright-report/
          retention-days: 7

      - name: Stop Docker Compose stack
        if: always()
        run: docker compose down

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: [lint-typecheck, test]
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '9.15.4'

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build all packages
        run: pnpm build

  docker:
    name: Build & Push Docker Images
    runs-on: ubuntu-latest
    needs: [build, e2e]
    outputs:
      tag: ${{ steps.version.outputs.tag }}
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Generate image tag
        id: version
        run: |
          # Always include short SHA so each build is immutable and cache-from can never
          # cross-contaminate between commits. For PRs the format is pr-N-sha7; for main
          # it is YYYY.MM.DD-sha7.
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            TAG="pr-${{ github.event.pull_request.number }}-${GITHUB_SHA::7}"
          else
            TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}"
          fi
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "Image tag: $TAG"

      - 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: Build and push API image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: apps/api/Dockerfile
          target: runner
          push: true
          tags: |
            ghcr.io/groombook/api:${{ steps.version.outputs.tag }}
            ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/api:latest' || '' }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Build and push Migrate image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: apps/api/Dockerfile
          target: migrate
          push: true
          tags: |
            ghcr.io/groombook/migrate:${{ steps.version.outputs.tag }}
            ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/migrate:latest' || '' }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Build and push Seed image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: apps/api/Dockerfile
          target: seed
          push: true
          tags: |
            ghcr.io/groombook/seed:${{ steps.version.outputs.tag }}
            ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/seed:latest' || '' }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Build and push Reset image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: apps/api/Dockerfile
          target: reset
          push: true
          tags: |
            ghcr.io/groombook/reset:${{ steps.version.outputs.tag }}
            ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/reset:latest' || '' }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Build and push Web image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: apps/web/Dockerfile
          push: true
          tags: |
            ghcr.io/groombook/web:${{ steps.version.outputs.tag }}
            ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/web:latest' || '' }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-dev:
    name: Deploy PR to groombook-dev
    runs-on: runners-groombook
    needs: [docker]
    if: github.event_name == 'pull_request'
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Install kubectl
        run: |
          curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
          chmod +x kubectl
          sudo mv kubectl /usr/local/bin/
          kubectl version --client

      - name: Deploy to groombook-dev
        env:
          PR_NUM: ${{ github.event.pull_request.number }}
          SHA: ${{ github.sha }}
        run: |
          TAG="pr-$PR_NUM-${SHA::7}"
          echo "Deploying images tagged $TAG to groombook-dev..."

          # Run migration with PR image
          kubectl delete job "migrate-pr-$PR_NUM" -n groombook-dev --ignore-not-found
          cat <<EOF | kubectl apply -n groombook-dev -f -
          apiVersion: batch/v1
          kind: Job
          metadata:
            name: migrate-pr-$PR_NUM
          spec:
            ttlSecondsAfterFinished: 3600
            backoffLimit: 2
            template:
              spec:
                restartPolicy: Never
                containers:
                - name: migrate
                  image: ghcr.io/groombook/migrate:$TAG
                  env:
                  - name: DATABASE_URL
                    valueFrom:
                      secretKeyRef:
                        name: groombook-postgres-credentials-dev
                        key: uri
          EOF
          kubectl wait --for=condition=complete "job/migrate-pr-$PR_NUM" \
            -n groombook-dev --timeout=120s

          # Update deployments
          kubectl set image deployment/api api=ghcr.io/groombook/api:$TAG -n groombook-dev
          kubectl set image deployment/web web=ghcr.io/groombook/web:$TAG -n groombook-dev

          # Wait for rollout
          kubectl rollout status deployment/api -n groombook-dev --timeout=300s
          kubectl rollout status deployment/web -n groombook-dev --timeout=300s

          echo "Deployment complete."

      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            const pr = context.issue.number;
            const tag = `pr-${pr}`;
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: pr,
              body: [
                '## Deployed to groombook-dev',
                '',
                `**Images:** \`${tag}\``,
                '**URL:** https://dev.groombook.farh.net',
                '',
                'Ready for UAT validation.'
              ].join('\n')
            });

  web-e2e:
    name: Web E2E (Dev)
    runs-on: ubuntu-latest
    needs: [docker, deploy-dev]
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '9.15.4'

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright browsers
        run: pnpm --filter @groombook/web exec playwright install --with-deps chromium

      - name: Run Web E2E tests
        run: pnpm --filter @groombook/web test:e2e
        timeout-minutes: 10

      - name: Upload Playwright report
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-web-e2e-report
          path: apps/web/playwright-report/
          retention-days: 7

  cd:
    name: Update Infra Image Tags
    runs-on: ubuntu-latest
    needs: [docker]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Generate infra repo token
        id: infra-token
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ vars.GH_APP_ID }}
          private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}

      - name: Clone groombook/infra
        run: |
          git clone https://x-access-token:${{ steps.infra-token.outputs.token }}@github.com/groombook/infra.git /tmp/infra

      - name: Install yq
        run: |
          sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
          sudo chmod +x /usr/local/bin/yq

      - name: Update dev overlay image tags
        env:
          TAG: ${{ needs.docker.outputs.tag }}
          SHA: ${{ github.sha }}
        run: |
          if [ -z "$TAG" ]; then
            TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}"
          fi
          export SHORT_SHA="${SHA::7}"
          echo "Updating dev overlay image tags to: $TAG"
          echo "Updating migration/seed Job names with SHA: $SHORT_SHA"
          cd /tmp/infra
          DEV_KUST="apps/groombook/overlays/dev/kustomization.yaml"
          yq -i '(.images[] | select(.name == "ghcr.io/groombook/api")).newTag = env(TAG)' "$DEV_KUST"
          yq -i '(.images[] | select(.name == "ghcr.io/groombook/web")).newTag = env(TAG)' "$DEV_KUST"
          yq -i '(.images[] | select(.name == "ghcr.io/groombook/migrate")).newTag = env(TAG)' "$DEV_KUST"
          yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$DEV_KUST"
          yq -i '(.images[] | select(.name == "ghcr.io/groombook/reset")).newTag = env(TAG)' "$DEV_KUST"

          # Update migrate Job name to include short SHA (immutable template fix)
          MIGRATE_JOB="apps/groombook/base/migrate-job.yaml"
          if [ -f "$MIGRATE_JOB" ]; then
            yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB"
            yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB"
            # Ensure ttlSecondsAfterFinished is set for automatic cleanup
            yq -i '.spec.ttlSecondsAfterFinished = (.spec.ttlSecondsAfterFinished // 86400)' "$MIGRATE_JOB"
          fi

          # Update seed Job name to include short SHA (immutable template fix)
          SEED_JOB="apps/groombook/base/seed-job.yaml"
          if [ -f "$SEED_JOB" ]; then
            yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB"
            yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB"
            # Ensure ttlSecondsAfterFinished is set for automatic cleanup
            yq -i '.spec.ttlSecondsAfterFinished = (.spec.ttlSecondsAfterFinished // 86400)' "$SEED_JOB"
          fi

          git -C /tmp/infra diff --stat

      - name: Create PR on groombook/infra
        env:
          TAG: ${{ needs.docker.outputs.tag }}
          GH_TOKEN: ${{ steps.infra-token.outputs.token }}
        run: |
          if [ -z "$TAG" ]; then
            TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}"
          fi

          cd /tmp/infra
          git config user.name "groombook-engineer[bot]"
          git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com"
          git checkout -b "chore/update-image-tags-${TAG}"
          git add apps/groombook/overlays/dev/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml
          git commit -m "chore: update image tags and migration/seed Job names to ${TAG}"

          git push -u origin "chore/update-image-tags-${TAG}"

          # Check if PR already exists for this branch
          EXISTING_PR=$(gh pr list --repo groombook/infra --head "chore/update-image-tags-${TAG}" --state open --json number -q '.[0].number' || true)
          if [ -n "$EXISTING_PR" ]; then
            echo "PR #$EXISTING_PR already exists for this tag, merging existing PR"
            gh pr merge "$EXISTING_PR" --repo groombook/infra --merge
          else
            PR_URL=$(gh pr create \
              --repo groombook/infra \
              --base main \
              --head "chore/update-image-tags-${TAG}" \
              --title "chore: deploy ${TAG} to dev" \
              --body "[GRO-178](/GRO/issues/GRO-178) — automated image tag update from main merge")
            gh pr merge "$PR_URL" --merge
          fi
