#!/usr/bin/env bash
set -euo pipefail

# cmk-register-worker
# - triggered by systemd timer (NO parameters)
# - processes queued registration requests created by cmk-push-agent-reg / cmk-agent-allow
# - serializes all work via flock (prevents conflicts)
# - ensures folders + hosts exist in Checkmk (per OMD site)
# - applies labels
# - activates changes once per site if:
#   - we changed something OR
#   - pending changes exist (possibly created by other user)
#
# Queue format:
#   /var/lib/cmkpush/register-queue/<site>/*.json
# JSON:
#   {"fqdn":"...","site":"<site>","folder":"/Path","labels":{...}}

CMK_USER="push_agent_register"

QUEUE_BASE="/var/lib/cmkpush/register-queue"
DONE_BASE="/var/lib/cmkpush/register-done"
FAIL_BASE="/var/lib/cmkpush/register-failed"
LOCK_FILE="/var/lock/cmk-register-worker.lock"

# --- helpers ---------------------------------------------------------------

api_base() {
  local site="$1"
  # Use HTTPS to avoid 302 redirect to https://127.0.0.1/<site>
  printf 'https://127.0.0.1/%s/check_mk/api/1.0' "$site"
}

secret_file() {
  local site="$1"
  printf '/omd/sites/%s/var/check_mk/web/%s/automation.secret' "$site" "$CMK_USER"
}

log()  { printf '%s\n' "$*" >&2; }
info() { log "INFO: $*"; }
warn() { log "WARN: $*"; }
err()  { log "ERROR: $*"; }

need_cmd() {
  command -v "$1" >/dev/null 2>&1 || { err "missing required command: $1"; exit 2; }
}

valid_site()   { [[ "${1:-}" =~ ^[A-Za-z0-9_][A-Za-z0-9_-]{0,31}$ ]]; }
valid_fqdn()   { [[ "${1:-}" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]]; }
valid_folder() { [[ "${1:-}" =~ ^/([A-Za-z0-9][A-Za-z0-9._-]{0,63})(/[A-Za-z0-9][A-Za-z0-9._-]{0,63})*$ ]]; }

# Only 2xx is success
api_ok() { [[ "${1:-}" =~ ^2 ]]; }

CURL_CODE=""
CURL_BODY=""

api_call() {
  local site="$1" method="$2" path="$3" data="${4:-}"
  local base url secret auth tmp

  base="$(api_base "$site")"
  url="${base}${path}"

  secret="$(<"$(secret_file "$site")")"
  auth="${CMK_USER}:${secret}"

  tmp="$(mktemp)"
  if [[ -n "$data" ]]; then
    CURL_CODE="$(
      curl -k -sS -o "$tmp" -w '%{http_code}' \
        -u "$auth" \
        -X "$method" \
        -H 'Accept: application/json' \
        -H 'Content-Type: application/json' \
        --data "$data" \
        "$url" || true
    )"
  else
    CURL_CODE="$(
      curl -k -sS -o "$tmp" -w '%{http_code}' \
        -u "$auth" \
        -X "$method" \
        -H 'Accept: application/json' \
        -H 'Content-Type: application/json' \
        "$url" || true
    )"
  fi

  CURL_BODY="$(cat "$tmp" 2>/dev/null || true)"
  rm -f "$tmp" >/dev/null 2>&1 || true
}

# ---------- Checkmk operations (REST) --------------------------------------

ensure_folder() {
  local site="$1" folder="$2"
  valid_folder "$folder" || { err "invalid folder: $folder"; return 1; }

  local IFS='/'
  read -r -a parts <<< "${folder#/}"
  local parent="/" name current="/"

  for name in "${parts[@]}"; do
    if [[ "$current" == "/" ]]; then
      parent="/"
      current="/$name"
    else
      parent="$current"
      current="$current/$name"
    fi

    local payload
    payload="$(jq -cn --arg parent "$parent" --arg name "$name" --arg title "$name" \
      '{name:$name,title:$title,parent:$parent}')"

    api_call "$site" "POST" "/domain-types/folder_config/collections/all" "$payload"

    if api_ok "$CURL_CODE"; then
      continue
    fi

    # Already exists -> ok (commonly 409)
    if [[ "$CURL_CODE" == "409" ]]; then
      continue
    fi

    # Some versions return 400 with a message that it exists
    if [[ "$CURL_CODE" == "400" ]] && echo "$CURL_BODY" | grep -qi 'exist'; then
      continue
    fi

    err "ensure_folder failed (site=$site folder=$current) http=$CURL_CODE body=$CURL_BODY"
    return 1
  done

  return 0
}

host_exists() {
  local site="$1" fqdn="$2"
  api_call "$site" "GET" "/objects/host_config/$fqdn"
  if [[ "$CURL_CODE" == "200" ]]; then return 0; fi
  if [[ "$CURL_CODE" == "404" ]]; then return 1; fi
  err "host_exists failed (site=$site host=$fqdn) http=$CURL_CODE body=$CURL_BODY"
  return 2
}

ensure_host() {
  local site="$1" fqdn="$2" folder="$3"
  valid_fqdn "$fqdn"   || { err "invalid fqdn: $fqdn"; return 1; }
  valid_folder "$folder" || { err "invalid folder: $folder"; return 1; }

  local ex=0
  if host_exists "$site" "$fqdn"; then
    return 0
  else
    ex=$?
    [[ $ex -eq 2 ]] && return 1
  fi

  local payload
  payload="$(jq -cn --arg folder "$folder" --arg host_name "$fqdn" \
    '{folder:$folder,host_name:$host_name,attributes:{ipaddress:""}}')"

  api_call "$site" "POST" "/domain-types/host_config/collections/all" "$payload"

  if api_ok "$CURL_CODE"; then return 0; fi
  if [[ "$CURL_CODE" == "409" ]]; then return 0; fi

  err "ensure_host failed (site=$site host=$fqdn folder=$folder) http=$CURL_CODE body=$CURL_BODY"
  return 1
}

set_labels() {
  local site="$1" fqdn="$2" labels_json="$3"

  local payload
  payload="$(jq -cn --argjson labels "$labels_json" '{attributes:{labels:$labels}}')"

  # Try PUT directly
  api_call "$site" "PUT" "/objects/host_config/$fqdn" "$payload"
  if api_ok "$CURL_CODE"; then return 0; fi

  # Fallback update action
  api_call "$site" "POST" "/objects/host_config/$fqdn/actions/update/invoke" "$payload"
  if api_ok "$CURL_CODE"; then return 0; fi

  warn "set_labels failed (site=$site host=$fqdn) http=$CURL_CODE body=$CURL_BODY"
  return 1
}

activate_changes() {
  local site="$1"

  # Many installs require If-Match for activation; do raw curl with header.
  local base url secret auth tmp
  base="$(api_base "$site")"
  url="${base}/domain-types/activation_run/actions/activate-changes/invoke"
  secret="$(<"$(secret_file "$site")")"
  auth="${CMK_USER}:${secret}"
  tmp="$(mktemp)"

  CURL_CODE="$(
    curl -k -sS -o "$tmp" -w '%{http_code}' \
      -u "$auth" -X POST \
      -H 'Accept: application/json' \
      -H 'Content-Type: application/json' \
      -H 'If-Match: *' \
      --data '{"redirect":false}' \
      "$url" || true
  )"
  CURL_BODY="$(cat "$tmp" 2>/dev/null || true)"
  rm -f "$tmp" >/dev/null 2>&1 || true

  if api_ok "$CURL_CODE"; then
    return 0
  fi

  err "activate_changes failed (site=$site) http=$CURL_CODE body=$CURL_BODY"
  return 1
}

# Tries to detect pending changes (including changes by other users).
# Returns:
#   0 -> pending changes exist
#   1 -> no pending changes
#   2 -> cannot determine (endpoint not available or unexpected response)
has_pending_changes() {
  local site="$1"

  # Try a commonly available endpoint in recent Checkmk REST API versions.
  api_call "$site" "GET" "/domain-types/activation_run/collections/pending_changes"

  if [[ "$CURL_CODE" == "200" ]]; then
    # body may contain "value" or "items" arrays depending on version
    if echo "$CURL_BODY" | jq -e '((.value // .items // []) | length) > 0' >/dev/null 2>&1; then
      return 0
    fi
    return 1
  fi

  # Endpoint not found / not supported -> unknown
  if [[ "$CURL_CODE" == "404" ]]; then
    return 2
  fi

  # Anything else -> unknown
  return 2
}

# ---------- Main ------------------------------------------------------------

need_cmd jq
need_cmd curl
need_cmd flock
need_cmd omd

install -d -m 0750 "$QUEUE_BASE" "$DONE_BASE" "$FAIL_BASE"

(
  flock -n 9 || exit 0
  shopt -s nullglob

  for sdir in "$QUEUE_BASE"/*; do
    [[ -d "$sdir" ]] || continue
    site="$(basename "$sdir")"

    if ! valid_site "$site"; then
      warn "skipping invalid site directory name: $site"
      continue
    fi

    if ! omd sites -b 2>/dev/null | grep -qx "$site"; then
      warn "skipping non-existent site: $site"
      continue
    fi

    sec="$(secret_file "$site")"
    if [[ ! -r "$sec" ]]; then
      err "missing automation secret for site=$site: $sec"
      continue
    fi

    done_dir="$DONE_BASE/$site"
    fail_dir="$FAIL_BASE/$site"
    install -d -m 0750 "$done_dir" "$fail_dir"

    files=("$sdir"/*.json)
    [[ ${#files[@]} -gt 0 ]] || continue

    info "processing site=$site items=${#files[@]}"
    changed=false

    for f in "${files[@]}"; do
      fqdn="$(jq -r '.fqdn // empty' "$f")"
      folder="$(jq -r '.folder // "/New"' "$f")"
      labels_json="$(jq -c '.labels // {}' "$f")"

      if ! valid_fqdn "$fqdn"; then
        warn "invalid fqdn in $f: $fqdn"
        mv -f "$f" "$fail_dir/invalid_$(basename "$f")"
        continue
      fi
      if ! valid_folder "$folder"; then
        warn "invalid folder in $f: $folder"
        mv -f "$f" "$fail_dir/invalid_$(basename "$f")"
        continue
      fi
      if ! echo "$labels_json" | jq -e 'type=="object"' >/dev/null; then
        warn "invalid labels in $f"
        mv -f "$f" "$fail_dir/invalid_$(basename "$f")"
        continue
      fi

      ok=false
      for attempt in 1 2 3 4 5; do
        if ensure_folder "$site" "$folder" && ensure_host "$site" "$fqdn" "$folder"; then
          set_labels "$site" "$fqdn" "$labels_json" || true
          ok=true
          break
        fi
        sleep "$attempt"
      done

      if $ok; then
        mv -f "$f" "$done_dir/$(basename "$f")"
        changed=true
      else
        mv -f "$f" "$fail_dir/retry_exhausted_$(basename "$f")"
      fi
    done

    # Activate changes:
    # - If we created/updated something: always activate
    # - Otherwise: activate if pending changes exist (might be from other user)
    if $changed; then
      info "activating changes for site=$site (worker processed items)"
      activate_changes "$site" || true
    else
      has_pending_changes "$site"
      rc=$?
      if [[ $rc -eq 0 ]]; then
        info "activating changes for site=$site (pending changes exist, possibly by other user)"
        activate_changes "$site" || true
      elif [[ $rc -eq 2 ]]; then
        # Can't reliably detect pending changes -> activate once per site run (since queue existed)
        info "activating changes for site=$site (cannot detect pending changes reliably)"
        activate_changes "$site" || true
      fi
    fi
  done
) 9>"$LOCK_FILE"

exit 0
