# services/comms/repair-jobs/othrys-kick-numeric-job.yaml
# One-off job for comms/othrys-kick-numeric-8.
# Purpose: othrys kick numeric 8 (see container args/env in this file).
# Run by setting spec.suspend to false, reconcile, then set it back to true.
# Safe to delete the finished Job/pod; it should not run continuously.
apiVersion: batch/v1
kind: Job
metadata:
  name: othrys-kick-numeric-8
  namespace: comms
spec:
  ttlSecondsAfterFinished: 3600
  suspend: true
  backoffLimit: 0
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/agent-pre-populate-only: "true"
        vault.hashicorp.com/role: "comms"
        vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret"
        vault.hashicorp.com/agent-inject-template-turn-secret: |
          {{- with secret "kv/data/atlas/comms/turn-shared-secret" -}}{{ .Data.data.TURN_STATIC_AUTH_SECRET }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-livekit-primary: "kv/data/atlas/comms/livekit-api"
        vault.hashicorp.com/agent-inject-template-livekit-primary: |
          {{- with secret "kv/data/atlas/comms/livekit-api" -}}{{ .Data.data.primary }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-bot-pass: "kv/data/atlas/comms/atlasbot-credentials-runtime"
        vault.hashicorp.com/agent-inject-template-bot-pass: |
          {{- with secret "kv/data/atlas/comms/atlasbot-credentials-runtime" -}}{{ index .Data.data "bot-password" }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-seeder-pass: "kv/data/atlas/comms/atlasbot-credentials-runtime"
        vault.hashicorp.com/agent-inject-template-seeder-pass: |
          {{- with secret "kv/data/atlas/comms/atlasbot-credentials-runtime" -}}{{ index .Data.data "seeder-password" }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-chat-matrix: "kv/data/atlas/shared/chat-ai-keys-runtime"
        vault.hashicorp.com/agent-inject-template-chat-matrix: |
          {{- with secret "kv/data/atlas/shared/chat-ai-keys-runtime" -}}{{ .Data.data.matrix }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-chat-homepage: "kv/data/atlas/shared/chat-ai-keys-runtime"
        vault.hashicorp.com/agent-inject-template-chat-homepage: |
          {{- with secret "kv/data/atlas/shared/chat-ai-keys-runtime" -}}{{ .Data.data.homepage }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-mas-admin-secret: "kv/data/atlas/comms/mas-admin-client-runtime"
        vault.hashicorp.com/agent-inject-template-mas-admin-secret: |
          {{- with secret "kv/data/atlas/comms/mas-admin-client-runtime" -}}{{ .Data.data.client_secret }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-synapse-db-pass: "kv/data/atlas/comms/synapse-db"
        vault.hashicorp.com/agent-inject-template-synapse-db-pass: |
          {{- with secret "kv/data/atlas/comms/synapse-db" -}}{{ .Data.data.POSTGRES_PASSWORD }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-mas-db-pass: "kv/data/atlas/comms/mas-db"
        vault.hashicorp.com/agent-inject-template-mas-db-pass: |
          {{- with secret "kv/data/atlas/comms/mas-db" -}}{{ .Data.data.password }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-mas-matrix-shared: "kv/data/atlas/comms/mas-secrets-runtime"
        vault.hashicorp.com/agent-inject-template-mas-matrix-shared: |
          {{- with secret "kv/data/atlas/comms/mas-secrets-runtime" -}}{{ .Data.data.matrix_shared_secret }}{{- end -}}
        vault.hashicorp.com/agent-inject-secret-mas-kc-secret: "kv/data/atlas/comms/mas-secrets-runtime"
        vault.hashicorp.com/agent-inject-template-mas-kc-secret: |
          {{- with secret "kv/data/atlas/comms/mas-secrets-runtime" -}}{{ .Data.data.keycloak_client_secret }}{{- end -}}
    spec:
      restartPolicy: Never
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node-role.kubernetes.io/worker
                    operator: Exists
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: kubernetes.io/arch
                    operator: In
                    values: ["arm64"]
      serviceAccountName: comms-vault
      containers:
        - name: kick
          image: python:3.11-slim
          env:
            - name: SYNAPSE_BASE
              value: http://othrys-synapse-matrix-synapse:8008
            - name: AUTH_BASE
              value: http://matrix-authentication-service:8080
            - name: SERVER_NAME
              value: live.bstein.dev
            - name: ROOM_ALIAS
              value: "#othrys:live.bstein.dev"
            - name: SEEDER_USER
              value: othrys-seeder
          command:
            - /bin/sh
            - -c
            - |
              set -euo pipefail
              . /vault/scripts/comms_vault_env.sh
              pip install --no-cache-dir requests >/dev/null
              python - <<'PY'
              import os
              import urllib.parse
              import requests

              BASE = os.environ["SYNAPSE_BASE"]
              AUTH_BASE = os.environ.get("AUTH_BASE", BASE)
              SERVER_NAME = os.environ.get("SERVER_NAME", "live.bstein.dev")
              ROOM_ALIAS = os.environ.get("ROOM_ALIAS", "#othrys:live.bstein.dev")
              SEEDER_USER = os.environ["SEEDER_USER"]
              SEEDER_PASS = os.environ["SEEDER_PASS"]

              def canon_user(user):
                  u = (user or "").strip()
                  if u.startswith("@") and ":" in u:
                      return u
                  u = u.lstrip("@")
                  if ":" in u:
                      return f"@{u}"
                  return f"@{u}:{SERVER_NAME}"

              def auth(token):
                  return {"Authorization": f"Bearer {token}"}

              def wait_for_service(url):
                  last = None
                  for attempt in range(1, 11):
                      try:
                          requests.get(url, timeout=10)
                          return
                      except Exception as exc:  # noqa: BLE001
                          last = exc
                          time.sleep(attempt * 2)
                  raise SystemExit(f"MAS service not reachable: {last}")

              def login(user, password):
                  r = requests.post(
                      f"{AUTH_BASE}/_matrix/client/v3/login",
                      json={
                          "type": "m.login.password",
                          "identifier": {"type": "m.id.user", "user": canon_user(user)},
                          "password": password,
                      },
                      timeout=30,
                  )
                  r.raise_for_status()
                  return r.json()["access_token"]

              def resolve_alias(token, alias):
                  enc = urllib.parse.quote(alias)
                  r = requests.get(f"{BASE}/_matrix/client/v3/directory/room/{enc}", headers=auth(token), timeout=30)
                  r.raise_for_status()
                  return r.json()["room_id"]

              def list_members(token, room_id):
                  r = requests.get(
                      f"{BASE}/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/members?membership=join",
                      headers=auth(token),
                      timeout=30,
                  )
                  r.raise_for_status()
                  members = []
                  for ev in r.json().get("chunk", []):
                      uid = ev.get("state_key")
                      if isinstance(uid, str) and uid.startswith("@"):
                          members.append(uid)
                  return members

              def is_numeric(user_id):
                  localpart = user_id.split(":", 1)[0].lstrip("@")
                  return localpart.isdigit()

              def kick(token, room_id, user_id):
                  r = requests.post(
                      f"{BASE}/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/kick",
                      headers=auth(token),
                      json={"user_id": user_id, "reason": "cleanup numeric guest"},
                      timeout=30,
                  )
                  if r.status_code not in (200, 202):
                      raise SystemExit(f"kick {user_id} failed: {r.status_code} {r.text}")

              wait_for_service(f"{AUTH_BASE}/_matrix/client/versions")
              token = login(SEEDER_USER, SEEDER_PASS)
              room_id = resolve_alias(token, ROOM_ALIAS)
              for user_id in list_members(token, room_id):
                  if user_id == canon_user(SEEDER_USER):
                      continue
                  if is_numeric(user_id):
                      kick(token, room_id, user_id)
              PY
          volumeMounts:
            - name: vault-scripts
              mountPath: /vault/scripts
              readOnly: true
      volumes:
        - name: vault-scripts
          configMap:
            name: comms-vault-env
            defaultMode: 0555
