name: deploy

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

jobs:
  test-backend:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: mangalord
          POSTGRES_PASSWORD: mangalord
          POSTGRES_DB: mangalord
        options: >-
          --health-cmd "pg_isready -U mangalord"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10
    env:
      DATABASE_URL: postgres://mangalord:mangalord@postgres:5432/mangalord
    steps:
      - uses: actions/checkout@v4
      # ubuntu-latest has node (so JS actions like checkout/cache run) but no
      # Rust. We intentionally avoid `container: rust:1-slim` because act_runner
      # runs JS actions with node *inside* the job container, and the slim Rust
      # image ships no node (checkout would fail with exit 127).
      - name: Install Rust + build deps
        run: |
          set -eu
          SUDO=""; [ "$(id -u)" = "0" ] || SUDO="sudo"
          $SUDO apt-get update
          $SUDO apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates curl
          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
          echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
      - name: Cache cargo registry and target
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            backend/target
          key: cargo-${{ runner.os }}-${{ hashFiles('backend/Cargo.lock') }}
          restore-keys: |
            cargo-${{ runner.os }}-
      - name: cargo test
        working-directory: backend
        run: cargo test --locked

  test-frontend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: npm
          cache-dependency-path: frontend/package-lock.json
      - name: npm ci
        working-directory: frontend
        run: npm ci
      - name: vitest
        working-directory: frontend
        run: npm test

  build-and-push:
    runs-on: ubuntu-latest
    needs: [test-backend, test-frontend]
    # PRs only run the test jobs; build + deploy are reserved for
    # post-merge pushes to main.
    if: github.event_name != 'pull_request'
    # Build on the host docker daemon directly (docker-outside-of-docker):
    # the runner shares the deploy host's daemon, so a plain `docker build`
    # reuses the host's layer cache and avoids buildx's docker-container
    # driver + the gha cache exporter — neither works against this single-host
    # act_runner, and there is no in-job daemon socket unless we mount it.
    container:
      image: docker.gitea.com/runner-images:ubuntu-latest
      volumes:
        - /var/run/docker.sock:/var/run/docker.sock
    outputs:
      image_tag: ${{ steps.meta.outputs.image_tag }}
      version: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - name: Resolve image tags
        id: meta
        run: |
          version="$(grep -m1 '^version' backend/Cargo.toml | cut -d'"' -f2)"
          frontend_version="$(grep -m1 '"version"' frontend/package.json | cut -d'"' -f4)"
          if [ "$version" != "$frontend_version" ]; then
            echo "Version mismatch: backend=$version frontend=$frontend_version" >&2
            exit 1
          fi
          echo "image_tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
          echo "version=${version}" >> "$GITHUB_OUTPUT"

      - name: Build & push backend + frontend
        env:
          REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
          REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
          IMAGE_TAG: ${{ steps.meta.outputs.image_tag }}
          VERSION: ${{ steps.meta.outputs.version }}
        run: |
          set -eu
          echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$REGISTRY_USERNAME" --password-stdin
          for svc in backend frontend; do
            img="$REGISTRY_URL/mangalord-$svc"
            # The backend bundles Debian's headless chromium for the crawler:
            # chromiumoxide's fetcher has no arm64 build, so the Pi must use a
            # system binary (pair with the runtime env
            # CRAWLER_CHROMIUM_BINARY=/usr/bin/chromium-headless-shell).
            build_args=""
            if [ "$svc" = "backend" ]; then build_args="--build-arg INSTALL_CHROMIUM=true"; fi
            docker build $build_args -t "$img:$IMAGE_TAG" -t "$img:latest" -t "$img:$VERSION" "./$svc"
            for tag in "$IMAGE_TAG" latest "$VERSION"; do docker push "$img:$tag"; done
          done
          docker logout "$REGISTRY_URL"

  deploy:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.event_name != 'pull_request'
    # Single-host deploy: the runner lives on the same box as the stack, so we
    # drive the host docker daemon directly (the job mounts the host docker
    # socket) instead of SSHing out. The compose dir is bind-mounted at its
    # REAL host path so compose's relative bind-mounts (./mangalord/...,
    # ./Caddyfile) resolve; both paths must be in the runner's
    # container.valid_volumes. The central compose references the images as
    # registry.mc02.dev/mangalord-*:${MANGALORD_TAG:-latest}, so we only pull
    # and recreate the two mangalord services at the freshly built SHA.
    container:
      image: docker:cli
      volumes:
        - /mnt/ssd/docker-data:/mnt/ssd/docker-data
        - /var/run/docker.sock:/var/run/docker.sock
    steps:
      - name: Deploy to the local stack
        working-directory: /mnt/ssd/docker-data
        env:
          REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
          REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
          IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }}
        run: |
          set -eu
          echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$REGISTRY_USERNAME" --password-stdin
          export MANGALORD_TAG="$IMAGE_TAG"
          docker compose pull mangalord-backend mangalord-frontend
          docker compose up -d mangalord-backend mangalord-frontend
          # Persist the deployed SHA into .env so it reflects what's actually
          # live. The export above is shell-only; without this, .env keeps its
          # `latest` placeholder and `docker inspect` is the only way to see the
          # running SHA. Runs only after a successful `up -d` (set -eu); kept
          # non-fatal so a write hiccup can't fail an otherwise-good deploy.
          sed -i "s/^MANGALORD_TAG=.*/MANGALORD_TAG=${IMAGE_TAG}/" .env \
            || echo "::warning::could not persist MANGALORD_TAG to .env"
          docker image prune -f
          docker logout "$REGISTRY_URL"
