name: Deploy Den

on:
  push:
    branches:
      - dev
    paths:
      - "services/den/**"
      - "services/den-worker-runtime/**"
      - ".github/workflows/deploy-den.yml"
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: deploy-den-${{ github.ref }}
  cancel-in-progress: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.repository == 'different-ai/openwork'
    steps:
      - name: Validate required secrets
        env:
          RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
          RENDER_DEN_CONTROL_PLANE_SERVICE_ID: ${{ secrets.RENDER_DEN_CONTROL_PLANE_SERVICE_ID }}
          RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
          DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
          DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
          DEN_GITHUB_CLIENT_ID: ${{ secrets.DEN_GITHUB_CLIENT_ID }}
          DEN_GITHUB_CLIENT_SECRET: ${{ secrets.DEN_GITHUB_CLIENT_SECRET }}
          DEN_GOOGLE_CLIENT_ID: ${{ secrets.DEN_GOOGLE_CLIENT_ID }}
          DEN_GOOGLE_CLIENT_SECRET: ${{ secrets.DEN_GOOGLE_CLIENT_SECRET }}
          POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
          POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
          POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX: ${{ vars.DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX }}
          DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
        run: |
          missing=0
          for key in RENDER_API_KEY RENDER_DEN_CONTROL_PLANE_SERVICE_ID RENDER_OWNER_ID DEN_DATABASE_URL DEN_BETTER_AUTH_SECRET; do
            if [ -z "${!key}" ]; then
              echo "::error::Missing required secret: $key"
              missing=1
            fi
          done

          vanity_suffix="${DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX:-openwork.studio}"
          if [ -n "$vanity_suffix" ] && [ -z "$VERCEL_TOKEN" ]; then
            echo "::error::Missing required secret: VERCEL_TOKEN (required when vanity domains are enabled)"
            missing=1
          fi

          feature_enabled="${DEN_POLAR_FEATURE_GATE_ENABLED:-false}"
          feature_enabled="$(echo "$feature_enabled" | tr '[:upper:]' '[:lower:]')"
          if [ "$feature_enabled" = "true" ]; then
            for key in POLAR_ACCESS_TOKEN POLAR_PRODUCT_ID POLAR_BENEFIT_ID; do
              if [ -z "${!key}" ]; then
                echo "::error::Missing required paywall secret: $key"
                missing=1
              fi
            done
          fi

          if [ -n "$DEN_GITHUB_CLIENT_ID" ] && [ -z "$DEN_GITHUB_CLIENT_SECRET" ]; then
            echo "::error::Missing required secret: DEN_GITHUB_CLIENT_SECRET (required when DEN_GITHUB_CLIENT_ID is set)"
            missing=1
          fi

          if [ -n "$DEN_GITHUB_CLIENT_SECRET" ] && [ -z "$DEN_GITHUB_CLIENT_ID" ]; then
            echo "::error::Missing required secret: DEN_GITHUB_CLIENT_ID (required when DEN_GITHUB_CLIENT_SECRET is set)"
            missing=1
          fi

          if [ -n "$DEN_GOOGLE_CLIENT_ID" ] && [ -z "$DEN_GOOGLE_CLIENT_SECRET" ]; then
            echo "::error::Missing required secret: DEN_GOOGLE_CLIENT_SECRET (required when DEN_GOOGLE_CLIENT_ID is set)"
            missing=1
          fi

          if [ -n "$DEN_GOOGLE_CLIENT_SECRET" ] && [ -z "$DEN_GOOGLE_CLIENT_ID" ]; then
            echo "::error::Missing required secret: DEN_GOOGLE_CLIENT_ID (required when DEN_GOOGLE_CLIENT_SECRET is set)"
            missing=1
          fi

          if [ "$missing" -ne 0 ]; then
            exit 1
          fi

      - name: Sync Render env vars and deploy latest commit
        env:
          RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
          RENDER_DEN_CONTROL_PLANE_SERVICE_ID: ${{ secrets.RENDER_DEN_CONTROL_PLANE_SERVICE_ID }}
          RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
          DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
          DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
          DEN_GITHUB_CLIENT_ID: ${{ secrets.DEN_GITHUB_CLIENT_ID }}
          DEN_GITHUB_CLIENT_SECRET: ${{ secrets.DEN_GITHUB_CLIENT_SECRET }}
          DEN_GOOGLE_CLIENT_ID: ${{ secrets.DEN_GOOGLE_CLIENT_ID }}
          DEN_GOOGLE_CLIENT_SECRET: ${{ secrets.DEN_GOOGLE_CLIENT_SECRET }}
          DEN_BETTER_AUTH_URL: ${{ vars.DEN_BETTER_AUTH_URL }}
          DEN_RENDER_WORKER_PLAN: ${{ vars.DEN_RENDER_WORKER_PLAN }}
          DEN_RENDER_WORKER_OPENWORK_VERSION: ${{ vars.DEN_RENDER_WORKER_OPENWORK_VERSION }}
          DEN_CORS_ORIGINS: ${{ vars.DEN_CORS_ORIGINS }}
          DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX: ${{ vars.DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX }}
          DEN_RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS: ${{ vars.DEN_RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS }}
          DEN_VERCEL_API_BASE: ${{ vars.DEN_VERCEL_API_BASE }}
          DEN_VERCEL_TEAM_ID: ${{ vars.DEN_VERCEL_TEAM_ID }}
          DEN_VERCEL_TEAM_SLUG: ${{ vars.DEN_VERCEL_TEAM_SLUG }}
          DEN_VERCEL_DNS_DOMAIN: ${{ vars.DEN_VERCEL_DNS_DOMAIN }}
          DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
          DEN_POLAR_API_BASE: ${{ vars.DEN_POLAR_API_BASE }}
          DEN_POLAR_SUCCESS_URL: ${{ vars.DEN_POLAR_SUCCESS_URL }}
          DEN_POLAR_RETURN_URL: ${{ vars.DEN_POLAR_RETURN_URL }}
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
          POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
          POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
        run: |
          python3 <<'PY'
          import json
          import os
          import time
          import urllib.error
          import urllib.parse
          import urllib.request

          api_key = os.environ["RENDER_API_KEY"]
          service_id = os.environ["RENDER_DEN_CONTROL_PLANE_SERVICE_ID"]
          owner_id = os.environ["RENDER_OWNER_ID"]
          openwork_version = os.environ.get("DEN_RENDER_WORKER_OPENWORK_VERSION") or "0.11.113"
          worker_plan = os.environ.get("DEN_RENDER_WORKER_PLAN") or "standard"
          configured_cors_origins = os.environ.get("DEN_CORS_ORIGINS") or ""
          worker_public_domain_suffix = os.environ.get("DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX") or "openwork.studio"
          custom_domain_ready_timeout_ms = os.environ.get("DEN_RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS") or "240000"
          vercel_api_base = os.environ.get("DEN_VERCEL_API_BASE") or "https://api.vercel.com"
          vercel_team_id = os.environ.get("DEN_VERCEL_TEAM_ID") or ""
          vercel_team_slug = os.environ.get("DEN_VERCEL_TEAM_SLUG") or "prologe"
          vercel_dns_domain = os.environ.get("DEN_VERCEL_DNS_DOMAIN") or worker_public_domain_suffix
          vercel_token = os.environ.get("VERCEL_TOKEN") or ""
          paywall_enabled = (os.environ.get("DEN_POLAR_FEATURE_GATE_ENABLED") or "false").lower() == "true"
          polar_api_base = os.environ.get("DEN_POLAR_API_BASE") or "https://api.polar.sh"
          polar_success_url = os.environ.get("DEN_POLAR_SUCCESS_URL") or "https://app.openwork.software"
          polar_return_url = os.environ.get("DEN_POLAR_RETURN_URL") or polar_success_url
          polar_access_token = os.environ.get("POLAR_ACCESS_TOKEN") or ""
          polar_product_id = os.environ.get("POLAR_PRODUCT_ID") or ""
          polar_benefit_id = os.environ.get("POLAR_BENEFIT_ID") or ""
          github_client_id = os.environ.get("DEN_GITHUB_CLIENT_ID") or ""
          github_client_secret = os.environ.get("DEN_GITHUB_CLIENT_SECRET") or ""
          google_client_id = os.environ.get("DEN_GOOGLE_CLIENT_ID") or ""
          google_client_secret = os.environ.get("DEN_GOOGLE_CLIENT_SECRET") or ""
          better_auth_url = os.environ.get("DEN_BETTER_AUTH_URL") or "https://app.openwork.software"

          if bool(github_client_id) != bool(github_client_secret):
              raise RuntimeError(
                  "DEN_GITHUB_CLIENT_ID and DEN_GITHUB_CLIENT_SECRET must either both be set or both be empty"
              )

          if bool(google_client_id) != bool(google_client_secret):
              raise RuntimeError(
                  "DEN_GOOGLE_CLIENT_ID and DEN_GOOGLE_CLIENT_SECRET must either both be set or both be empty"
              )

          def validate_redirect_url(name: str, value: str):
              parsed = urllib.parse.urlparse(value)
              if parsed.scheme not in {"http", "https"} or not parsed.netloc:
                  raise RuntimeError(f"{name} must be an absolute http(s) URL, got: {value}")

          validate_redirect_url("DEN_POLAR_SUCCESS_URL", polar_success_url)
          validate_redirect_url("DEN_POLAR_RETURN_URL", polar_return_url)
          validate_redirect_url("DEN_BETTER_AUTH_URL", better_auth_url)

          if paywall_enabled and (not polar_access_token or not polar_product_id or not polar_benefit_id):
              raise RuntimeError(
                  "DEN_POLAR_FEATURE_GATE_ENABLED=true requires POLAR_ACCESS_TOKEN, POLAR_PRODUCT_ID, and POLAR_BENEFIT_ID"
              )

          def normalize_origin(value: str) -> str:
              trimmed = value.strip()
              if trimmed == "*":
                  return trimmed
              return trimmed.rstrip("/")

          def build_cors_origins(raw: str, defaults: list[str]) -> str:
              candidates: list[str] = []
              if raw.strip():
                  candidates.extend(raw.split(","))
              candidates.extend(defaults)

              seen = set()
              normalized = []
              for value in candidates:
                  origin = normalize_origin(value)
                  if not origin or origin in seen:
                      continue
                  seen.add(origin)
                  normalized.append(origin)

              if not normalized:
                  raise RuntimeError("Unable to derive CORS_ORIGINS for Den deployment")

              return ",".join(normalized)

          headers = {
              "Authorization": f"Bearer {api_key}",
              "Accept": "application/json",
              "Content-Type": "application/json",
          }

          def request(method: str, path: str, body=None):
              url = f"https://api.render.com/v1{path}"
              data = None
              if body is not None:
                  data = json.dumps(body).encode("utf-8")
              req = urllib.request.Request(url, data=data, method=method, headers=headers)
              try:
                  with urllib.request.urlopen(req, timeout=60) as resp:
                      text = resp.read().decode("utf-8")
                      return resp.status, json.loads(text) if text else None
              except urllib.error.HTTPError as err:
                  text = err.read().decode("utf-8", "replace")
                  raise RuntimeError(f"{method} {path} failed ({err.code}): {text[:600]}")

          _, service = request("GET", f"/services/{service_id}")
          service_url = (service.get("serviceDetails") or {}).get("url")
          if not service_url:
              raise RuntimeError(f"Render service {service_id} has no public URL")

          cors_origins = build_cors_origins(
              configured_cors_origins,
              [
                  "https://app.openwork.software",
                  "https://api.openwork.software",
                  service_url,
              ],
          )

          env_vars = [
              {"key": "DATABASE_URL", "value": os.environ["DEN_DATABASE_URL"]},
              {"key": "BETTER_AUTH_SECRET", "value": os.environ["DEN_BETTER_AUTH_SECRET"]},
              {"key": "BETTER_AUTH_URL", "value": better_auth_url},
              {"key": "GITHUB_CLIENT_ID", "value": github_client_id},
              {"key": "GITHUB_CLIENT_SECRET", "value": github_client_secret},
              {"key": "GOOGLE_CLIENT_ID", "value": google_client_id},
              {"key": "GOOGLE_CLIENT_SECRET", "value": google_client_secret},
              {"key": "CORS_ORIGINS", "value": cors_origins},
              {"key": "PROVISIONER_MODE", "value": "render"},
              {"key": "RENDER_API_BASE", "value": "https://api.render.com/v1"},
              {"key": "RENDER_API_KEY", "value": api_key},
              {"key": "RENDER_OWNER_ID", "value": owner_id},
              {"key": "RENDER_WORKER_REPO", "value": "https://github.com/different-ai/openwork"},
              {"key": "RENDER_WORKER_BRANCH", "value": "dev"},
              {"key": "RENDER_WORKER_ROOT_DIR", "value": "services/den-worker-runtime"},
              {"key": "RENDER_WORKER_PLAN", "value": worker_plan},
              {"key": "RENDER_WORKER_REGION", "value": "oregon"},
              {"key": "RENDER_WORKER_OPENWORK_VERSION", "value": openwork_version},
              {"key": "RENDER_WORKER_NAME_PREFIX", "value": "den-worker-openwork"},
              {"key": "RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX", "value": worker_public_domain_suffix},
              {"key": "RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS", "value": custom_domain_ready_timeout_ms},
              {"key": "RENDER_PROVISION_TIMEOUT_MS", "value": "900000"},
              {"key": "RENDER_HEALTHCHECK_TIMEOUT_MS", "value": "180000"},
              {"key": "RENDER_POLL_INTERVAL_MS", "value": "5000"},
              {"key": "VERCEL_API_BASE", "value": vercel_api_base},
              {"key": "VERCEL_TOKEN", "value": vercel_token},
              {"key": "VERCEL_TEAM_ID", "value": vercel_team_id},
              {"key": "VERCEL_TEAM_SLUG", "value": vercel_team_slug},
              {"key": "VERCEL_DNS_DOMAIN", "value": vercel_dns_domain},
              {"key": "POLAR_FEATURE_GATE_ENABLED", "value": "true" if paywall_enabled else "false"},
              {"key": "POLAR_API_BASE", "value": polar_api_base},
              {"key": "POLAR_ACCESS_TOKEN", "value": polar_access_token},
              {"key": "POLAR_PRODUCT_ID", "value": polar_product_id},
              {"key": "POLAR_BENEFIT_ID", "value": polar_benefit_id},
              {"key": "POLAR_SUCCESS_URL", "value": polar_success_url},
              {"key": "POLAR_RETURN_URL", "value": polar_return_url},
          ]

          request("PUT", f"/services/{service_id}/env-vars", env_vars)
          _, deploy = request("POST", f"/services/{service_id}/deploys", {})
          deploy_id = deploy.get("id") or (deploy.get("deploy") or {}).get("id")
          if not deploy_id:
              raise RuntimeError(f"Unexpected deploy response: {deploy}")

          terminal = {"live", "update_failed", "build_failed", "canceled"}
          started = time.time()

          while time.time() - started < 1800:
              _, deploys = request("GET", f"/services/{service_id}/deploys?limit=1")
              latest = deploys[0]["deploy"] if deploys else None
              if latest and latest.get("id") == deploy_id and latest.get("status") in terminal:
                  status = latest.get("status")
                  if status != "live":
                      raise RuntimeError(f"Render deploy {deploy_id} ended with {status}")
                  print(f"Render deploy {deploy_id} is live at {service_url}")
                  break
              time.sleep(10)
          else:
              raise RuntimeError(f"Timed out waiting for deploy {deploy_id}")
          PY
