stages:
  - test
  - build
  - deploy

variables:
  DOCKERFILE_PATH: Dockerfile.production
  DEPLOY_ROOT: /opt/rentaldrivego
  # Required: disable TLS so the docker client can reach the dind daemon
  DOCKER_TLS_CERTDIR: ""
  DOCKER_HOST: tcp://docker:2375

# ====================================
# TEST STAGE
# Runs on every push and merge request
# ====================================

api_tests:
  stage: test
  image: node:20-bookworm
  before_script:
    - npm ci
    - npm run db:generate
  script:
    - npm run type-check
    - npm run test:api
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

storefront_tests:
  stage: test
  image: node:20-bookworm
  before_script:
    - npm ci
    - npm run build --workspace @rentaldrivego/types
  script:
    - npm run test:storefront
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

admin_tests:
  stage: test
  image: node:20-bookworm
  before_script:
    - npm ci
  script:
    - npm run test:admin
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

dashboard_tests:
  stage: test
  image: node:20-bookworm
  before_script:
    - npm ci
    - npm run build --workspace @rentaldrivego/types
  script:
    - npm run test:dashboard
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

integration_tests:
  stage: test
  image: node:20-bookworm
  services:
    - name: postgres:16-alpine
      alias: postgres
      variables:
        POSTGRES_DB: rentaldrivego_test
        POSTGRES_USER: postgres
        POSTGRES_PASSWORD: password
    - name: redis:7-alpine
      alias: redis
  variables:
    # Used by db:deploy (Prisma migrations) and any code reading DATABASE_URL directly
    DATABASE_URL: "postgresql://postgres:password@postgres:5432/rentaldrivego_test"
    REDIS_URL: "redis://redis:6379"
    NODE_ENV: test
  before_script:
    - npm ci
    # Overwrite the local .env.test so vitest integration config gets CI service hostnames
    # (the file uses postgres/redis aliases, not localhost)
    - |
      cat > apps/api/.env.test << 'EOF'
      DATABASE_URL=postgresql://postgres:password@postgres:5432/rentaldrivego_test
      REDIS_URL=redis://redis:6379
      JWT_SECRET=test-secret
      JWT_EXPIRY=8h
      NODE_ENV=test
      FILE_STORAGE_ROOT=/tmp/rentaldrivego-test-storage
      EOF
  script:
    - npm run db:generate
    - npm run db:deploy
    - npx prisma db push --schema packages/database/prisma/schema.prisma
    - npm run test:api:integration
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

# ====================================
# BUILD STAGE
# ====================================

build_image:
  stage: build
  image: docker:24
  services:
    - name: docker:24-dind
      alias: docker
      command: ["--tls=false"]
      variables:
        HEALTHCHECK_TCP_PORT: "2375"
  before_script:
    - |
      attempts=0
      until docker info >/dev/null 2>&1; do
        attempts=$((attempts + 1))
        if [ "$attempts" -ge 60 ]; then
          echo "Docker daemon did not become ready after 60 seconds." >&2
          echo "On self-hosted runners, docker:dind requires the runner Docker executor to run with privileged = true." >&2
          exit 1
        fi
        echo "Waiting for Docker daemon..."
        sleep 1
      done
    # Use one registry mode at a time:
    # 1) explicit REGISTRY_* vars for external/custom registries
    # 2) GitLab's built-in CI_REGISTRY_* vars for the integrated registry
    - |
      if [ -n "${REGISTRY_HOST:-}${REGISTRY_USER:-}${REGISTRY_PASSWORD:-}${REGISTRY_IMAGE:-}" ]; then
        test -n "${REGISTRY_HOST:-}" || { echo "REGISTRY_HOST is not set. Configure all REGISTRY_* variables together."; exit 1; }
        test -n "${REGISTRY_USER:-}" || { echo "REGISTRY_USER is not set. Configure all REGISTRY_* variables together."; exit 1; }
        test -n "${REGISTRY_PASSWORD:-}" || { echo "REGISTRY_PASSWORD is not set. Configure all REGISTRY_* variables together."; exit 1; }
        test -n "${REGISTRY_IMAGE:-}" || { echo "REGISTRY_IMAGE is not set. Configure all REGISTRY_* variables together."; exit 1; }
        export IMAGE_REPOSITORY="$REGISTRY_IMAGE"
      else
        export REGISTRY_HOST="${CI_REGISTRY:-}"
        export REGISTRY_USER="${CI_REGISTRY_USER:-}"
        export REGISTRY_PASSWORD="${CI_REGISTRY_PASSWORD:-}"
        export IMAGE_REPOSITORY="${CI_REGISTRY_IMAGE:-}"
      fi
    - test -n "$REGISTRY_HOST" || { echo "Registry host is not set. Configure CI_REGISTRY or REGISTRY_HOST."; exit 1; }
    - test -n "$REGISTRY_USER" || { echo "Registry user is not set. Configure CI_REGISTRY_USER or REGISTRY_USER."; exit 1; }
    - test -n "$REGISTRY_PASSWORD" || { echo "Registry password is not set. Configure CI_REGISTRY_PASSWORD or REGISTRY_PASSWORD."; exit 1; }
    - test -n "$IMAGE_REPOSITORY" || { echo "Image repository is not set. Configure CI_REGISTRY_IMAGE or REGISTRY_IMAGE."; exit 1; }
    - export DOCKER_IMAGE="$IMAGE_REPOSITORY:$CI_COMMIT_SHORT_SHA"
    - export DOCKER_IMAGE_LATEST="$IMAGE_REPOSITORY:latest"
    - |
      echo "--- Registry Connectivity Preflight ---"
      echo "REGISTRY_HOST=$REGISTRY_HOST"
      cat /etc/resolv.conf || true

      if command -v getent >/dev/null 2>&1; then
        getent hosts "$REGISTRY_HOST" || true
      elif command -v nslookup >/dev/null 2>&1; then
        nslookup "$REGISTRY_HOST" || true
      else
        echo "No host lookup tool available in runner image."
      fi

      probe_output="$(wget -S --spider --timeout=15 --tries=1 "https://$REGISTRY_HOST/v2/" 2>&1 || true)"
      printf '%s\n' "$probe_output"

      printf '%s\n' "$probe_output" | grep -F "401 Unauthorized" >/dev/null || {
        echo "Registry preflight failed: expected HTTPS /v2/ to return 401 Unauthorized before docker login." >&2
        exit 1
      }

      if ! printf '%s\n' "$probe_output" | grep -Fi "Docker-Distribution-Api-Version: registry/2.0" >/dev/null; then
        echo "Registry preflight note: registry API header was not visible in wget output; continuing because HTTPS /v2/ returned 401." >&2
      fi
    - echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin
  script:
    - echo "--- Building Docker Image ---"
    - |
      for var_name in NEXT_PUBLIC_API_URL NEXT_PUBLIC_MARKETPLACE_URL NEXT_PUBLIC_DASHBOARD_URL NEXT_PUBLIC_ADMIN_URL; do
        var_value="$(printenv "$var_name" || true)"

        if [ -z "$var_value" ]; then
          echo "$var_name is not set. Refusing to build a production image with missing public frontend URLs." >&2
          exit 1
        fi

        case "$var_value" in
          *example.com*)
            echo "$var_name=$var_value still uses the placeholder example.com domain. Update the GitLab CI/CD variable before building." >&2
            exit 1
            ;;
        esac
      done
    # NEXT_PUBLIC_* vars are inlined into Next.js bundles at build time — must be passed as ARGs
    - |
      export API_INTERNAL_URL="${API_INTERNAL_URL:-http://api:4000/api/v1}"
      export DASHBOARD_INTERNAL_URL="${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}"
      export ADMIN_INTERNAL_URL="${ADMIN_INTERNAL_URL:-http://admin:3002}"

      docker build \
        --build-arg API_INTERNAL_URL="$API_INTERNAL_URL" \
        --build-arg DASHBOARD_INTERNAL_URL="$DASHBOARD_INTERNAL_URL" \
        --build-arg ADMIN_INTERNAL_URL="$ADMIN_INTERNAL_URL" \
        --build-arg NEXT_PUBLIC_API_URL="$NEXT_PUBLIC_API_URL" \
        --build-arg NEXT_PUBLIC_MARKETPLACE_URL="$NEXT_PUBLIC_MARKETPLACE_URL" \
        --build-arg NEXT_PUBLIC_DASHBOARD_URL="$NEXT_PUBLIC_DASHBOARD_URL" \
        --build-arg NEXT_PUBLIC_ADMIN_URL="$NEXT_PUBLIC_ADMIN_URL" \
        -t "$DOCKER_IMAGE" \
        -t "$DOCKER_IMAGE_LATEST" \
        -f "$DOCKERFILE_PATH" .
    - echo "--- Pushing to Registry ---"
    - docker push "$DOCKER_IMAGE"
    - docker push "$DOCKER_IMAGE_LATEST"
  rules:
    # On the default branch, always attempt the image build so missing registry
    # configuration fails loudly in before_script instead of skipping the job.
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - when: never

# ====================================
# DEPLOY STAGE
# ====================================

deploy_to_vps:
  stage: deploy
  image: alpine:latest
  needs:
    - build_image
  before_script:
    - |
      if [ -n "${REGISTRY_HOST:-}${REGISTRY_USER:-}${REGISTRY_PASSWORD:-}${REGISTRY_IMAGE:-}" ]; then
        test -n "${REGISTRY_HOST:-}" || { echo "REGISTRY_HOST is not set. Configure all REGISTRY_* variables together."; exit 1; }
        test -n "${REGISTRY_USER:-}" || { echo "REGISTRY_USER is not set. Configure all REGISTRY_* variables together."; exit 1; }
        test -n "${REGISTRY_PASSWORD:-}" || { echo "REGISTRY_PASSWORD is not set. Configure all REGISTRY_* variables together."; exit 1; }
        test -n "${REGISTRY_IMAGE:-}" || { echo "REGISTRY_IMAGE is not set. Configure all REGISTRY_* variables together."; exit 1; }
        export IMAGE_REPOSITORY="$REGISTRY_IMAGE"
      else
        export REGISTRY_HOST="${CI_REGISTRY:-}"
        export REGISTRY_USER="${CI_REGISTRY_USER:-}"
        export REGISTRY_PASSWORD="${CI_REGISTRY_PASSWORD:-}"
        export IMAGE_REPOSITORY="${CI_REGISTRY_IMAGE:-}"
      fi
    - test -n "$REGISTRY_HOST" || { echo "Registry host is not set. Configure CI_REGISTRY or REGISTRY_HOST."; exit 1; }
    - test -n "$REGISTRY_USER" || { echo "Registry user is not set. Configure CI_REGISTRY_USER or REGISTRY_USER."; exit 1; }
    - test -n "$REGISTRY_PASSWORD" || { echo "Registry password is not set. Configure CI_REGISTRY_PASSWORD or REGISTRY_PASSWORD."; exit 1; }
    - test -n "$IMAGE_REPOSITORY" || { echo "Image repository is not set. Configure CI_REGISTRY_IMAGE or REGISTRY_IMAGE."; exit 1; }
    - export DOCKER_IMAGE="$IMAGE_REPOSITORY:$CI_COMMIT_SHORT_SHA"
    - apk add --no-cache openssh-client
    # Load the SSH private key stored in the CI variable SSH_PRIVATE_KEY
    - eval "$(ssh-agent -s)"
    - |
      key_file="$(mktemp)"
      decoded_key_file="$(mktemp)"
      escaped_key_file="$(mktemp)"

      cleanup_key_files() {
        rm -f "$key_file" "$decoded_key_file" "$escaped_key_file"
      }
      trap cleanup_key_files EXIT

      if [ -f "${SSH_PRIVATE_KEY:-}" ]; then
        tr -d '\r' < "$SSH_PRIVATE_KEY" > "$key_file"
      else
        printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$key_file"
      fi

      chmod 600 "$key_file"

      if ! ssh-keygen -y -f "$key_file" >/dev/null 2>&1; then
        if [ ! -f "${SSH_PRIVATE_KEY:-}" ]; then
          printf '%b' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$escaped_key_file"
          if ssh-keygen -y -f "$escaped_key_file" >/dev/null 2>&1; then
            cp "$escaped_key_file" "$key_file"
          elif printf '%s' "$SSH_PRIVATE_KEY" | tr -d '\r\n\t ' | base64 -d > "$decoded_key_file" 2>/dev/null; then
            tr -d '\r' < "$decoded_key_file" > "$key_file"
          fi
          chmod 600 "$key_file"
        fi
      fi

      ssh-keygen -y -f "$key_file" >/dev/null 2>&1 || {
        echo "SSH_PRIVATE_KEY must be an unencrypted OpenSSH private key. Supported formats: GitLab file variable, raw multiline key, single-line key with literal \\n escapes, or base64-encoded key." >&2
        exit 1
      }

      ssh-add "$key_file"
      cleanup_key_files
      trap - EXIT
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    # Scan and trust the VPS host key (avoids manual known_hosts management)
    - ssh-keyscan -H "$VPS_IP" >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
  script:
    - echo "--- Deploying $DOCKER_IMAGE to VPS ---"
    - |
      REGISTRY_PASSWORD_B64="$(printf '%s' "$REGISTRY_PASSWORD" | base64 | tr -d '\n')"

      echo "-- Syncing deployment assets --"
      ssh "$VPS_USER@$VPS_IP" "
        set -e
        mkdir -p '$DEPLOY_ROOT/scripts' '$DEPLOY_ROOT/docker/pgmanage' '$DEPLOY_ROOT/docker/registry/auth' '$DEPLOY_ROOT/dynamic'
      "
      scp docker-compose.production.yml docker-compose.portainer.production.yml docker-compose.registry.production.yml docker-compose.registry.local.yml traefik.yaml "$VPS_USER@$VPS_IP:$DEPLOY_ROOT/"
      scp scripts/docker-prod-common.sh scripts/docker-prod-deploy.sh scripts/docker-prod-up-registry.sh scripts/docker-registry-local-up.sh "$VPS_USER@$VPS_IP:$DEPLOY_ROOT/scripts/"
      scp docker/pgmanage/override.py "$VPS_USER@$VPS_IP:$DEPLOY_ROOT/docker/pgmanage/"

      echo "-- Running deploy script on VPS --"
      ssh "$VPS_USER@$VPS_IP" "
        set -e
        cd '$DEPLOY_ROOT'
        APP_IMAGE='$IMAGE_REPOSITORY' \
        APP_VERSION='$CI_COMMIT_SHORT_SHA' \
        REGISTRY_HOST='$REGISTRY_HOST' \
        REGISTRY_USER='$REGISTRY_USER' \
        REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \
        bash scripts/docker-prod-deploy.sh
      "
  rules:
    # Deploy only when both registry access and VPS credentials are available.
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && ((($CI_REGISTRY && $CI_REGISTRY_USER && $CI_REGISTRY_PASSWORD && $CI_REGISTRY_IMAGE) || ($REGISTRY_HOST && $REGISTRY_USER && $REGISTRY_PASSWORD && $REGISTRY_IMAGE)) && $SSH_PRIVATE_KEY && $VPS_IP && $VPS_USER)'
      when: manual
    - when: never
