#!/usr/bin/env bash
#
# provision-uptime.sh — idempotent provisioner for Better Stack synthetic
# monitors covering humanwork-api, humanwork-agent, and humanwork-frontend.
#
# What it provisions:
#   - 3 status monitors (api/health, agent/health, frontend root) probing
#     from 3 regions, 30s interval (60s for frontend), with 2-consecutive-
#     fail confirmation.
#   - Slack delivery on each monitor (#ops-alerts via stored webhook).
#
# What it does NOT provision (operator does this once in the UI):
#   - The on-call rotation (Better Stack -> On-call -> New rotation).
#   - The Slack incoming-webhook URL (paste into Integrations -> Slack).
#   - The Better Stack -> Slack integration mapping itself.
#
# Idempotency: looks up existing monitors by `pronounceable_name`. If a
# monitor with that name exists, the script PATCHes its config to match
# this file. If not, it POSTs a new one. Re-running is safe.
#
# Usage:
#   export BETTERSTACK_API_TOKEN=<team API token>
#   export BETTERSTACK_ENV=prod   # or staging
#   bash scripts/provision-uptime.sh
#
# Exit codes:
#   0  — all monitors present and matching desired config
#   1  — env var missing or token invalid (sanity check failure)
#   2  — Better Stack API returned an error during provisioning we can't recover from

set -euo pipefail

if [[ -z "${BETTERSTACK_API_TOKEN:-}" ]]; then
  echo "ERROR: BETTERSTACK_API_TOKEN not set" >&2
  echo "Get one from https://uptime.betterstack.com/team/<team>/api-tokens" >&2
  exit 1
fi

ENVIRONMENT="${BETTERSTACK_ENV:-prod}"
API_BASE="https://uptime.betterstack.com/api/v2"

case "$ENVIRONMENT" in
  prod)
    API_HOST="api.h.work"
    AGENT_HOST="agent.h.work"
    FRONTEND_HOST="app.h.work"
    ;;
  staging)
    API_HOST="api-staging.h.work"
    AGENT_HOST="agent-staging.h.work"
    FRONTEND_HOST="app-staging.h.work"
    ;;
  *)
    echo "ERROR: BETTERSTACK_ENV must be 'prod' or 'staging', got '$ENVIRONMENT'" >&2
    exit 1
    ;;
esac

NAME_PREFIX="humanwork-${ENVIRONMENT}"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
bs_get() {
  curl -fsSL -H "Authorization: Bearer $BETTERSTACK_API_TOKEN" "$API_BASE/$1"
}

bs_post() {
  curl -fsSL -X POST \
    -H "Authorization: Bearer $BETTERSTACK_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$2" \
    "$API_BASE/$1"
}

bs_patch() {
  curl -fsSL -X PATCH \
    -H "Authorization: Bearer $BETTERSTACK_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$2" \
    "$API_BASE/$1"
}

# Find a monitor by pronounceable_name. Args: pname. Echos the monitor id
# (or empty string when no match). Returns 0 on success, 1 when the API
# call failed — the caller MUST abort on 1, otherwise a transient Better
# Stack outage produces a duplicate monitor on the POST branch (breaks
# idempotency). Follows pagination via `pagination.next` URL until exhausted
# or a match is found; do not assume per_page=250 covers the whole team.
find_monitor_id() {
  local pname="$1"
  local url="monitors?per_page=250"
  while [[ -n "$url" ]]; do
    local body
    if ! body="$(bs_get "$url")"; then
      return 1
    fi
    # Inline match check + emit next-page URL on stdout via separator.
    # Python writes EITHER "FOUND <id>" OR "NEXT <relative_url>" OR nothing.
    #
    # rc capture: `out="$(...)"` swallows the inner command's exit code, so a
    # python crash (SyntaxError, OOM, JSON decode of malformed body) would
    # leave $out empty and fall into the `*)` branch — the caller would treat
    # it as "no match" and POST a duplicate monitor (idempotency bug
    # flagged on PR #1307 by macroscope). Capture rc via tempfile + read so
    # crashes propagate as `return 1` (which the caller already aborts on).
    local out rc rc_file
    rc_file="$(mktemp)"
    out="$(
      { python3 -c '
import json, sys, urllib.parse
body = json.loads(sys.stdin.read() or "{}")
pname = sys.argv[1]
for m in body.get("data", []):
    if m.get("attributes", {}).get("pronounceable_name") == pname:
        print("FOUND", m.get("id"))
        sys.exit(0)
nxt = body.get("pagination", {}).get("next") or ""
if nxt:
    parsed = urllib.parse.urlparse(nxt)
    rel = parsed.path
    if parsed.query:
        rel += "?" + parsed.query
    # Strip /api/v2/ prefix so it composes with API_BASE
    if rel.startswith("/api/v2/"):
        rel = rel[len("/api/v2/"):]
    print("NEXT", rel)
' "$pname" <<<"$body"; echo $? >"$rc_file"; }
    )"
    rc="$(cat "$rc_file" 2>/dev/null || echo 1)"
    rm -f "$rc_file"
    if [[ "$rc" != "0" ]]; then
      echo "ERROR: pagination match check (python3) crashed with rc=$rc — refusing to POST to avoid duplicate." >&2
      return 1
    fi
    case "$out" in
      FOUND\ *) echo "${out#FOUND }"; return 0 ;;
      NEXT\ *)  url="${out#NEXT }" ;;
      *)        return 0 ;;  # end of pagination, no match
    esac
  done
  return 0
}

# Provision or update one monitor. Args: pname, url, interval, regions(json).
provision_monitor() {
  local pname="$1" url="$2" interval="$3" regions_json="$4" expected_codes="$5"

  # Same rc-capture pattern as find_monitor_id — `desired=$(...)` would
  # otherwise swallow a python crash (e.g. invalid REGIONS/CODES JSON from
  # the caller, or an unexpected env break) and we'd POST/PATCH an empty
  # body. Better Stack would reject it with 400, but it surfaces as the
  # opaque bs_post/bs_patch error; this gives a precise local error.
  local desired rc rc_file
  rc_file="$(mktemp)"
  desired=$(
    { PNAME="$pname" URL="$url" INTERVAL="$interval" REGIONS="$regions_json" CODES="$expected_codes" \
      python3 -c '
import json, os
print(json.dumps({
  "url": os.environ["URL"],
  "pronounceable_name": os.environ["PNAME"],
  "monitor_type": "status",
  "check_frequency": int(os.environ["INTERVAL"]),
  "request_timeout": 15,
  "confirmation_period": 60,
  "regions": json.loads(os.environ["REGIONS"]),
  "expected_status_codes": json.loads(os.environ["CODES"]),
  "follow_redirects": True,
  "verify_ssl": True,
  "email": False,
  "sms": True,
  "push": True,
  "team_wait": 180,
}))
'; echo $? >"$rc_file"; }
  )
  rc="$(cat "$rc_file" 2>/dev/null || echo 1)"
  rm -f "$rc_file"
  if [[ "$rc" != "0" ]]; then
    echo "ERROR: failed to build monitor payload for '$pname' (python3 rc=$rc). Refusing to POST." >&2
    exit 2
  fi

  local existing_id
  if ! existing_id="$(find_monitor_id "$pname")"; then
    # Better Stack API call failed. DO NOT POST a new monitor (would create
    # a duplicate). Exit with the documented "API error" code (2).
    echo "ERROR: monitor lookup for '$pname' failed; refusing to POST to avoid duplicate." >&2
    exit 2
  fi

  if [[ -n "$existing_id" ]]; then
    echo "PATCH monitor[$existing_id] $pname"
    if ! bs_patch "monitors/$existing_id" "$desired" >/dev/null; then
      echo "ERROR: PATCH monitor[$existing_id] '$pname' failed." >&2
      exit 2
    fi
  else
    echo "POST  monitor $pname"
    local resp
    if ! resp="$(bs_post 'monitors' "$desired")"; then
      echo "ERROR: POST monitor '$pname' failed." >&2
      exit 2
    fi
    local new_id
    if ! new_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["id"])' <<<"$resp")"; then
      # POST succeeded on Better Stack side, but the response shape was
      # unexpected — surface a precise message rather than logging an empty id.
      # No retry: the monitor IS created; an operator can run the script again
      # and the find_monitor_id path will pick it up and PATCH instead of POST.
      echo "WARNING: POST monitor '$pname' succeeded on Better Stack but response did not match expected shape; rerun this script to PATCH it idempotently." >&2
      new_id="(unknown — check Better Stack UI)"
    fi
    echo "      created id=$new_id"
  fi
}

# ---------------------------------------------------------------------------
# Sanity check the token
# ---------------------------------------------------------------------------
echo "Probing Better Stack API ..."
if ! bs_get 'monitors?per_page=1' >/dev/null; then
  echo "ERROR: Better Stack API rejected the token. Check BETTERSTACK_API_TOKEN." >&2
  exit 1
fi
echo "Token OK."

# ---------------------------------------------------------------------------
# Provision
# ---------------------------------------------------------------------------
provision_monitor \
  "${NAME_PREFIX}-api" \
  "https://${API_HOST}/health" \
  30 \
  '["us","eu","as"]' \
  '[200]'

provision_monitor \
  "${NAME_PREFIX}-agent" \
  "https://${AGENT_HOST}/health" \
  30 \
  '["us","eu","as"]' \
  '[200]'

provision_monitor \
  "${NAME_PREFIX}-frontend" \
  "https://${FRONTEND_HOST}/" \
  60 \
  '["us","eu"]' \
  '[200,301,302]'

echo ""
echo "Done. Verify in https://uptime.betterstack.com/team/<team>/monitors"
echo "Remember to wire on-call rotation + Slack integration in the UI (one-time)."
