name: Docker Build and Push

on:
  push:
    branches: [ "main" ]
    paths:
      # Note: C++ changes trigger shardok_arm64_build.yml instead
      # Note: Auth changes trigger auth_build.yml instead
      # Note: Windows installer changes trigger installer_build.yml instead
      - 'src/main/go/**'
      - '!src/main/go/net/eagle0/authservice/**'
      - '!src/main/go/net/eagle0/authcli/**'
      - '!src/main/go/net/eagle0/clients/**'
      - 'src/main/scala/**'
      - 'src/main/protobuf/**'
      - '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
      - '!src/main/protobuf/net/eagle0/eagle/api/admin/**'
      - '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
      - 'src/main/resources/**'
      - 'ci/BUILD.bazel'
      - 'MODULE.bazel'
      - '.bazelrc'
      - 'docker-compose.prod.yml'
      - 'nginx/**'
      - '.github/workflows/docker_build.yml'
  workflow_dispatch:
    inputs:
      push_images:
        description: 'Push images to container registry'
        required: true
        default: 'false'
        type: boolean

# Only allow one deployment at a time to prevent race conditions
concurrency:
  group: docker-build-deploy
  cancel-in-progress: false  # Don't cancel running deployments, queue new ones

permissions:
  contents: read

jobs:
  # Single consolidated build job - builds all images with one bazel invocation
  # This uses 1 runner slot instead of 4, and Bazel parallelizes internally
  # NOTE: Must run on a runner with the bazel label to ensure consistent Bazel cache
  build-all:
    runs-on: [self-hosted, bazel]
    outputs:
      eagle_image_tag: ${{ steps.push-images.outputs.eagle_image_tag }}
      admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
      jfr_sidecar_image_tag: ${{ steps.push-images.outputs.jfr_sidecar_image_tag }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          lfs: false

      - name: Build all Docker images
        id: build-all
        run: |
          set -ex

          # Build ALL images in a single bazel command - Bazel parallelizes internally
          # Note: Shardok is built separately for ARM64 and deployed to Hetzner
          echo "=== Building Docker images ==="
          bazel build \
            --platforms=//:linux_x86_64 \
            --extra_toolchains=@llvm_toolchain_linux//:all \
            //ci:eagle_server_image \
            //ci:admin_server_image \
            //ci:jfr_sidecar_image \
            //ci:warmup_tar

          # Extract warmup binary from tar for deployment
          mkdir -p scripts/bin
          tar -xf bazel-bin/ci/warmup_tar.tar -C scripts/bin --strip-components=1

          # Save all image paths before any other bazel command changes bazel-bin symlink
          EAGLE_PATH=$(readlink -f bazel-bin/ci/eagle_server_image)
          ADMIN_PATH=$(readlink -f bazel-bin/ci/admin_server_image)
          JFR_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)

          echo "eagle_path=$EAGLE_PATH" >> $GITHUB_OUTPUT
          echo "admin_path=$ADMIN_PATH" >> $GITHUB_OUTPUT
          echo "jfr_path=$JFR_PATH" >> $GITHUB_OUTPUT

          echo "=== Image paths ==="
          echo "Eagle: $EAGLE_PATH"
          echo "Admin: $ADMIN_PATH"
          echo "JFR Sidecar: $JFR_PATH"

      - name: Login to DigitalOcean Container Registry
        if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
        env:
          DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
        run: |
          mkdir -p ~/.docker
          AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
          echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
          mkdir -p .docker
          cp ~/.docker/config.json .docker/

      - name: Push all images to DO registry
        id: push-images
        if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
        env:
          DOCKER_CONFIG: ${{ github.workspace }}/.docker
        run: |
          set -ex

          GIT_SHA=$(git rev-parse --short=8 HEAD)

          # Get crane from push target runfiles
          bazel build //ci:eagle_server_push
          RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
          CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)

          if [ -z "$CRANE" ] || [ ! -x "$CRANE" ]; then
            echo "ERROR: crane not found in runfiles"
            find "$RUNFILES" -name crane 2>/dev/null || echo "No crane found at all"
            exit 1
          fi
          echo "Using crane: $CRANE"

          # Push Eagle image
          EAGLE_IMAGE="${{ steps.build-all.outputs.eagle_path }}"
          EAGLE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
          echo "Pushing Eagle: $EAGLE_TAG"
          $CRANE push "$EAGLE_IMAGE" "$EAGLE_TAG"
          $CRANE copy "$EAGLE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
          echo "eagle_image_tag=$EAGLE_TAG" >> $GITHUB_OUTPUT

          # Push Admin image
          ADMIN_IMAGE="${{ steps.build-all.outputs.admin_path }}"
          ADMIN_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
          echo "Pushing Admin: $ADMIN_TAG"
          $CRANE push "$ADMIN_IMAGE" "$ADMIN_TAG"
          $CRANE copy "$ADMIN_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
          echo "admin_image_tag=$ADMIN_TAG" >> $GITHUB_OUTPUT

          # Push JFR Sidecar image
          JFR_IMAGE="${{ steps.build-all.outputs.jfr_path }}"
          JFR_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
          echo "Pushing JFR Sidecar: $JFR_TAG"
          $CRANE push "$JFR_IMAGE" "$JFR_TAG"
          $CRANE copy "$JFR_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
          echo "jfr_sidecar_image_tag=$JFR_TAG" >> $GITHUB_OUTPUT

          echo "=== All images pushed successfully ==="

  deploy:
    runs-on: [self-hosted, bazel]
    needs: [build-all]
    if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
    environment: production
    env:
      EAGLE_IMAGE: ${{ needs.build-all.outputs.eagle_image_tag }}
      ADMIN_IMAGE: ${{ needs.build-all.outputs.admin_image_tag }}
      JFR_SIDECAR_IMAGE: ${{ needs.build-all.outputs.jfr_sidecar_image_tag }}
      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
      GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
      EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
      DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
      DO_SPACES_SECRET_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
      # Admin server uses different credentials (for eagle0-assets bucket)
      ADMIN_S3_ACCESS_KEY: ${{ secrets.ACCESS_KEY_ID }}
      ADMIN_S3_SECRET_KEY: ${{ secrets.SECRET_KEY }}
      JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
      DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
      DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
      GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
      GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
      GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
      GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
      APPLE_SIGNIN_CLIENT_ID: ${{ secrets.APPLE_SIGNIN_CLIENT_ID }}
      APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
      APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
      APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
      TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
      TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
      SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
      SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
      SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
      FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
      FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
      FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
      DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
      DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
      NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DO_SSH_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true

      - name: Build warmup tool
        run: |
          bazel build //ci:warmup_tar
          mkdir -p scripts/bin
          tar -xf bazel-bin/ci/warmup_tar.tar -C scripts/bin --strip-components=1

      - name: Copy config files to droplet
        run: |
          # Create directory structure on remote
          ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
          set -e
          mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx
          rm -f /opt/eagle0/scripts/bin/warmup
          SETUP_DIRS

          # Copy files
          scp -i ~/.ssh/deploy_key docker-compose.prod.yml deploy@"$DO_DROPLET_IP":/opt/eagle0/
          scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
          scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh scripts/eagle-exec.sh scripts/eagle-logs.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
          scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/

      - name: Deploy to production droplet
        run: |
          ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << DEPLOY_SCRIPT
          set -ex
          cd /opt/eagle0

          # =================================================================
          # CRITICAL: Validate environment variables before proceeding
          # This catches GitHub Actions secret store hiccups early
          # =================================================================
          validate_env() {
              local var_name="\$1"
              local var_value="\$2"
              local default_value="\${3:-}"

              if [ -z "\${var_value}" ]; then
                  echo "ERROR: \${var_name} is empty. GitHub Actions secrets may have failed to load."
                  echo "Please retry the workflow."
                  return 1
              fi

              # Check if we got a default value instead of the real secret
              if [ -n "\${default_value}" ] && [ "\${var_value}" = "\${default_value}" ]; then
                  echo "ERROR: \${var_name} has default value '\${default_value}' instead of the actual secret."
                  echo "This indicates GitHub Actions secrets failed to load. Please retry the workflow."
                  return 1
              fi

              return 0
          }

          echo "Validating critical environment variables..."

          # These are the raw values from GitHub Actions (before export)
          # We check them before exporting to catch issues early
          VALIDATION_FAILED=0

          validate_env "SHARDOK_ADDRESS" "${SHARDOK_ADDRESS}" "" || VALIDATION_FAILED=1
          validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
          validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
          validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1

          if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
              echo ""
              echo "========================================="
              echo "DEPLOYMENT ABORTED: Missing critical secrets"
              echo "This is likely a transient GitHub Actions issue."
              echo "Please retry the workflow."
              echo "========================================="
              exit 1
          fi

          echo "All critical environment variables validated successfully."

          # =================================================================
          # Export environment variables for docker compose
          # These are passed via heredoc and exported so child processes (docker compose) can access them
          export EAGLE_IMAGE="${EAGLE_IMAGE}"
          export ADMIN_IMAGE="${ADMIN_IMAGE}"
          export JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
          export OPENAI_API_KEY="${OPENAI_API_KEY}"
          export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
          export GEMINI_API_KEY="${GEMINI_API_KEY}"
          export GPT_MODEL_NAME="${GPT_MODEL_NAME:-gpt-4o}"
          export EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3:-false}"
          export DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
          export DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
          export ADMIN_S3_ACCESS_KEY="${ADMIN_S3_ACCESS_KEY}"
          export ADMIN_S3_SECRET_KEY="${ADMIN_S3_SECRET_KEY}"
          export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
          export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
          export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
          export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
          export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
          export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
          export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
          export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
          export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
          export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
          export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
          export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
          export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
          export SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
          export SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
          export SENTRY_DSN="${SENTRY_DSN}"
          export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
          export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
          export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
          export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
          export NOTIFY_SECRET="${NOTIFY_SECRET}"

          # Check Docker has IPv6 support
          if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
            echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
          fi

          # Login to registry
          echo "\$DO_REGISTRY_TOKEN" | docker login registry.digitalocean.com -u "\$DO_REGISTRY_TOKEN" --password-stdin

          echo "Using images: \$EAGLE_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"

          # Install crane for pulling OCI images
          echo "Installing crane..."
          rm -f crane
          curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
          chmod +x crane

          # Pull and load all images
          echo "Pulling Eagle image..."
          ./crane pull "\${EAGLE_IMAGE}" eagle.tar && docker load -i eagle.tar && rm eagle.tar

          echo "Pulling Admin image..."
          ./crane pull "\${ADMIN_IMAGE}" admin.tar && docker load -i admin.tar && rm admin.tar
          # Tag as :latest locally so docker-compose fallback uses correct image
          docker tag "\${ADMIN_IMAGE}" registry.digitalocean.com/eagle0/admin-server:latest

          echo "Pulling JFR Sidecar image..."
          ./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar && docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar

          # Pull other compose images
          docker pull nginx:alpine || true
          docker pull certbot/certbot || true

          echo "All images pulled successfully"

          # =================================================================
          # Verify Shardok connectivity before proceeding with deployment
          # This catches network/firewall issues early
          # =================================================================
          echo "Verifying Shardok connectivity..."
          SHARDOK_HOST=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f1)
          SHARDOK_PORT=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f2)

          # Try to connect to Shardok (timeout after 10 seconds)
          if nc -z -w 10 "\${SHARDOK_HOST}" "\${SHARDOK_PORT}" 2>/dev/null; then
              echo "Shardok connectivity verified: \${SHARDOK_ADDRESS} is reachable"
          else
              echo ""
              echo "========================================="
              echo "ERROR: Cannot reach Shardok at \${SHARDOK_ADDRESS}"
              echo "This may indicate:"
              echo "  - Shardok server is not running on Hetzner"
              echo "  - Network/firewall issues between DigitalOcean and Hetzner"
              echo "  - Incorrect SHARDOK_ADDRESS configuration"
              echo ""
              echo "DEPLOYMENT ABORTED: Shardok must be reachable for battles to work."
              echo "========================================="
              exit 1
          fi

          # Stop local shardok container if running (now runs on Hetzner)
          docker stop shardok-server 2>/dev/null || true
          docker rm shardok-server 2>/dev/null || true

          # Deploy Eagle with blue-green (handles eagle, nginx, admin, jfr-sidecar)
          # Note: Shardok runs on Hetzner, deployed separately via shardok_arm64_build.yml
          # Note: Auth is deployed separately via auth_build.yml - do NOT touch auth here
          chmod +x /opt/eagle0/scripts/*.sh
          [ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
          GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
          /opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}"

          # Verify
          sleep 10
          docker compose -f docker-compose.prod.yml ps
          docker compose -f docker-compose.prod.yml images

          # Verify admin container is running the correct image
          echo "=== Verifying admin container image ==="
          ADMIN_RUNNING_DIGEST=\$(docker inspect admin-server --format '{{.Image}}')
          ADMIN_EXPECTED_DIGEST=\$(docker inspect "\${ADMIN_IMAGE}" --format '{{.Id}}')
          echo "Expected image: \${ADMIN_IMAGE}"
          echo "Expected digest: \${ADMIN_EXPECTED_DIGEST}"
          echo "Running digest:  \${ADMIN_RUNNING_DIGEST}"
          if [ "\${ADMIN_RUNNING_DIGEST}" != "\${ADMIN_EXPECTED_DIGEST}" ]; then
            echo "ERROR: Admin container is running wrong image!"
            echo "Container entrypoint:"
            docker inspect admin-server --format '{{.Config.Entrypoint}}'
            exit 1
          fi
          echo "Admin image verification passed"

          # Cleanup - remove old images to prevent disk full
          docker container prune -f
          docker image prune -f
          # Remove images older than 24h (keeps recent images for rollback)
          docker image prune -a -f --filter "until=24h" || true
          DEPLOY_SCRIPT
