# Build images, migrate, deploy on version tags.
# Requires act_runner with docker, curl, ssh, scp, bash and label ubuntu-latest.
#
# Secrets: REGISTRY_USERNAME, REGISTRY_TOKEN, PG_CONNECTION_DDL,
#          DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY
# Variables: REGISTRY (optional; defaults below)
name: CD

on:
  push:
    tags: ["v*"]
  workflow_dispatch:
    inputs:
      image_tag:
        description: "Image tag to deploy (defaults to the pushed tag)"
        required: false

env:
  REGISTRY: git.vectur45.com/trent/vigilcare-records

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.meta.outputs.tag }}
    steps:
      - uses: actions/checkout@v4

      - name: Resolve tag and registry
        id: meta
        run: |
          if [ -n "${{ vars.REGISTRY }}" ]; then
            echo "REGISTRY=${{ vars.REGISTRY }}" >> "$GITHUB_ENV"
          fi
          if [ -n "${{ inputs.image_tag }}" ]; then
            echo "tag=${{ inputs.image_tag }}" >> "$GITHUB_OUTPUT"
          elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
            echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
          else
            echo "image_tag input is required for workflow_dispatch without a tag" >&2
            exit 1
          fi

      - name: Log in to the Gitea registry
        run: |
          echo "${{ secrets.REGISTRY_TOKEN }}" \
            | docker login "${REGISTRY%%/*}" \
              -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin

      - name: Build and push vigilcare-records-api
        run: |
          docker build -f VigilCareRecordsAPI/Dockerfile \
            -t "${REGISTRY}/vigilcare-records-api:${{ steps.meta.outputs.tag }}" \
            -t "${REGISTRY}/vigilcare-records-api:latest" .
          docker push "${REGISTRY}/vigilcare-records-api:${{ steps.meta.outputs.tag }}"
          docker push "${REGISTRY}/vigilcare-records-api:latest"

      - name: Build and push vigilcare-records-dashboard
        # Context is vigilcare-records-web/ — package.json and nginx.conf live there.
        run: |
          docker build -f vigilcare-records-web/Dockerfile \
            -t "${REGISTRY}/vigilcare-records-dashboard:${{ steps.meta.outputs.tag }}" \
            -t "${REGISTRY}/vigilcare-records-dashboard:latest" vigilcare-records-web
          docker push "${REGISTRY}/vigilcare-records-dashboard:${{ steps.meta.outputs.tag }}"
          docker push "${REGISTRY}/vigilcare-records-dashboard:latest"

  migrate:
    needs: build-and-push
    runs-on: ubuntu-latest
    # Checkout must run on the job host. Build the EF bundle via Dockerfile
    # --target migrate (context upload), not docker run -v — under act_runner
    # bind mounts resolve on the Docker host, not the job workspace.
    # Postgres lives on the deploy host's shared-services network and is not
    # reachable from act_runner, so the bundle is copied there and executed in
    # a one-shot container joined to that network (Host=postgres resolves).
    # NOTE: Program.cs also runs db.Database.MigrateAsync() on API startup, so
    # this job is a defense-in-depth pre-deploy step using a DDL-privileged
    # credential the API container never sees, not the only migration path.
    steps:
      - uses: actions/checkout@v4

      - name: Build migration bundle
        run: |
          docker build -f VigilCareRecordsAPI/Dockerfile --target migrate \
            -t vigilcare-records-migrate-bundle:local .
          cid=$(docker create vigilcare-records-migrate-bundle:local)
          docker cp "$cid:/out/migrate-api" ./migrate-api
          docker rm "$cid"
          chmod +x ./migrate-api

      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts

      # Runs while the previous release is still serving traffic, so every
      # migration must be backwards-compatible with the outgoing image
      # (expand-then-contract). Connection strings contain `;` / spaces
      # ("SSL Mode=...") — never pass them as a bare ssh remote argv; the
      # remote shell splits on `;` and drops the variable (set -u → unbound).
      - name: Apply migrations
        env:
          PG_CONNECTION_DDL: ${{ secrets.PG_CONNECTION_DDL }}
        run: |
          REMOTE="${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}"
          scp -i ~/.ssh/id_ed25519 ./migrate-api \
            "$REMOTE:/tmp/vigilcare-records-migrate-api"
          umask 077
          printf '%s\n' "$PG_CONNECTION_DDL" > /tmp/vigilcare-records-pg-ddl
          scp -i ~/.ssh/id_ed25519 /tmp/vigilcare-records-pg-ddl \
            "$REMOTE:/tmp/vigilcare-records-pg-ddl"
          rm -f /tmp/vigilcare-records-pg-ddl
          ssh -i ~/.ssh/id_ed25519 "$REMOTE" bash -euo pipefail <<'EOF'
            chmod +x /tmp/vigilcare-records-migrate-api
            chmod 600 /tmp/vigilcare-records-pg-ddl
            PG_CONNECTION_DDL="$(cat /tmp/vigilcare-records-pg-ddl)"
            docker run --rm \
              --network shared-services \
              -v /tmp/vigilcare-records-migrate-api:/migrate-api:ro \
              --entrypoint /migrate-api \
              mcr.microsoft.com/dotnet/runtime-deps:8.0 \
              --connection "$PG_CONNECTION_DDL"
            rm -f /tmp/vigilcare-records-migrate-api /tmp/vigilcare-records-pg-ddl
          EOF

  deploy:
    needs: [build-and-push, migrate]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts

      - name: Copy compose file
        run: |
          scp -i ~/.ssh/id_ed25519 docker-compose.prod.yml \
            "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/opt/vigilcare-records/docker-compose.prod.yml"

      - name: Deploy
        env:
          IMAGE_TAG: ${{ needs.build-and-push.outputs.tag }}
        run: |
          ssh -i ~/.ssh/id_ed25519 \
            "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
            IMAGE_TAG="$IMAGE_TAG" bash -euo pipefail <<'EOF'
            cd /opt/vigilcare-records

            # Record the currently deployed tag so a rollback has a target.
            grep '^IMAGE_TAG=' .env > .env.previous || true

            if grep -q '^IMAGE_TAG=' .env; then
              sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${IMAGE_TAG}|" .env
            else
              echo "IMAGE_TAG=${IMAGE_TAG}" >> .env
            fi

            docker compose -f docker-compose.prod.yml --env-file .env pull
            docker compose -f docker-compose.prod.yml --env-file .env up -d --remove-orphans
            docker image prune -f
          EOF

      - name: Smoke test
        run: |
          ssh -i ~/.ssh/id_ed25519 \
            "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" bash -euo pipefail <<'EOF'
            cd /opt/vigilcare-records
            # Do not `source` .env — compose env files are not bash (semicolons,
            # spaces in "SSL Mode=...", CRLF). Read only the host ports we need.
            env_val() { sed -n "s/^${1}=//p" .env | tail -n1 | tr -d '\r'; }
            API_PORT="$(env_val API_PORT)"; API_PORT="${API_PORT:-5217}"
            DASHBOARD_PORT="$(env_val DASHBOARD_PORT)"; DASHBOARD_PORT="${DASHBOARD_PORT:-8089}"

            for i in $(seq 1 30); do
              if curl -fsS "http://localhost:${API_PORT}/health/ready" >/dev/null; then
                echo "Ready check passed."
                curl -fsS "http://localhost:${DASHBOARD_PORT}/" >/dev/null && echo "Dashboard serving."
                exit 0
              fi
              sleep 5
            done
            echo "Ready check never passed — dumping API logs:"
            docker compose -f docker-compose.prod.yml --env-file .env logs --tail 100 api
            exit 1
          EOF

      - name: Roll back on failure
        if: failure()
        run: |
          ssh -i ~/.ssh/id_ed25519 \
            "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" bash -euo pipefail <<'EOF'
            cd /opt/vigilcare-records
            # Restores the previous image tag only. Schema changes are NOT
            # reverted — this is why migrations must be backwards-compatible.
            if [ -f .env.previous ]; then
              PREV=$(cut -d= -f2 .env.previous)
              sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${PREV}|" .env
              docker compose -f docker-compose.prod.yml --env-file .env up -d
              echo "Rolled back to ${PREV}"
            fi
          EOF
