name: CI

on:
  pull_request:
  push:
    branches: [master]
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  check:
    runs-on: ubuntu-latest

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

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install uv
        run: python -m pip install --upgrade pip uv

      - name: Cache uv
        uses: actions/cache@v4
        with:
          path: |
            ~/.cache/uv
            backend/.venv
          key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }}

      - name: Set up Node
        id: setup-node
        uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
          cache-dependency-path: frontend/package-lock.json

      - name: Install backend dependencies
        run: make backend-sync

      - name: Install frontend dependencies
        run: make frontend-sync

      - name: Cache Next.js build cache
        uses: actions/cache@v4
        with:
          path: |
            frontend/.next/cache
          key: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-



      - name: Enforce one migration per PR
        if: ${{ github.event_name == 'pull_request' }}
        env:
          GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
        run: |
          ./scripts/ci/one_migration_per_pr.sh

      - name: Run migration integrity gate
        if: ${{ github.event_name == 'pull_request' }}
        run: |
          set -euo pipefail

          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE_SHA="${{ github.event.pull_request.base.sha }}"
            HEAD_SHA="${{ github.sha }}"
            git fetch --no-tags --depth=1 origin "$BASE_SHA"
          else
            BASE_SHA="${{ github.event.before }}"
            HEAD_SHA="${{ github.sha }}"
          fi

          CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
          echo "Changed files:"
          echo "$CHANGED_FILES"

          if ! echo "$CHANGED_FILES" | grep -Eq '^backend/(app/models|db|migrations|alembic\.ini)'; then
            echo "No migration-relevant backend changes detected; skipping migration gate."
            exit 0
          fi

          if echo "$CHANGED_FILES" | grep -Eq '^backend/app/models/' && ! echo "$CHANGED_FILES" | grep -Eq '^backend/migrations/versions/'; then
            echo "Model changes detected without a migration under backend/migrations/versions/."
            exit 1
          fi

          make backend-migration-check

      - name: Run backend checks
        env:
          # Keep CI builds deterministic.
          NEXT_TELEMETRY_DISABLED: "1"
          AUTH_MODE: "clerk"
          CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
        run: |
          make backend-lint
          make backend-typecheck
          make backend-coverage

      - name: Run frontend checks
        env:
          # Keep CI builds deterministic.
          NEXT_TELEMETRY_DISABLED: "1"
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_AUTH_MODE: "clerk"
          CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
          NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
        run: |
          make frontend-lint
          make frontend-typecheck
          make frontend-test
          make frontend-build


      - name: Docs quality gates (lint + relative link check)
        run: |
          make docs-check

      - name: Upload coverage artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          if-no-files-found: ignore
          path: |
            backend/coverage.xml
            frontend/coverage/**

  installer:
    runs-on: ubuntu-latest
    needs: [check]

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

      - name: Validate installer shell syntax
        run: bash -n install.sh

      - name: Installer smoke test (docker mode)
        run: |
          ./install.sh \
            --mode docker \
            --backend-port 18000 \
            --frontend-port 13000 \
            --public-host localhost \
            --api-url http://localhost:18000 \
            --token-mode generate
          curl -fsS http://127.0.0.1:18000/healthz >/dev/null
          curl -fsS http://127.0.0.1:13000 >/dev/null

      - name: Cleanup docker stack after docker mode
        if: always()
        run: |
          docker compose -f compose.yml --env-file .env down -v --remove-orphans || true

      - name: Installer smoke test (local mode)
        run: |
          ./install.sh \
            --mode local \
            --backend-port 18001 \
            --frontend-port 13001 \
            --public-host localhost \
            --api-url http://localhost:18001 \
            --token-mode generate \
            --db-mode docker \
            --start-services yes
          curl -fsS http://127.0.0.1:18001/healthz >/dev/null
          curl -fsS http://127.0.0.1:13001 >/dev/null

      - name: Cleanup local processes and docker resources
        if: always()
        run: |
          if [ -f .install-logs/backend.pid ]; then kill "$(cat .install-logs/backend.pid)" || true; fi
          if [ -f .install-logs/frontend.pid ]; then kill "$(cat .install-logs/frontend.pid)" || true; fi
          docker compose -f compose.yml --env-file .env down -v --remove-orphans || true

  e2e:
    runs-on: ubuntu-latest
    needs: [check]

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

      - name: Set up Node
        id: setup-node
        uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
          cache-dependency-path: frontend/package-lock.json

      - name: Install frontend dependencies
        run: make frontend-sync

      - name: Cache Next.js build cache
        uses: actions/cache@v4
        with:
          path: |
            frontend/.next/cache
          key: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-


      - name: Start frontend (dev server)
        env:
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_AUTH_MODE: "clerk"
          NEXT_TELEMETRY_DISABLED: "1"
          CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
          NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
        run: |
          cd frontend
          npm run dev -- --hostname 0.0.0.0 --port 3000 &
          for i in {1..60}; do
            if curl -sf http://localhost:3000/ > /dev/null; then exit 0; fi
            sleep 2
          done
          echo "Frontend did not start"
          exit 1

      - name: Run Cypress E2E
        env:
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_AUTH_MODE: "clerk"
          NEXT_TELEMETRY_DISABLED: "1"
          # Clerk testing tokens (official @clerk/testing Cypress integration)
          CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
          # Also set for the app itself.
          NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
        run: |
          cd frontend
          npm run e2e -- --browser chrome

      - name: Upload Cypress artifacts
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: cypress-artifacts
          if-no-files-found: ignore
          path: |
            frontend/cypress/screenshots/**
            frontend/cypress/videos/**
