#!/usr/bin/env bash
# Build the new index for a Path B rebuild and wait until its indexer has caught up
# (PB-RAG-09-01 steps 5 and 6). It never touches the alias: switch with switch_alias.py.
#
# Usage:   bash reindex.sh <index-definition.json> <indexer-definition.json>
#          bash reindex.sh defs/after/index-kb-v2.json defs/after/indexer-kb-v2.json
# Needs:   SEARCH_ENDPOINT, API_VERSION, ALIAS. TOKEN is optional (taken from az if unset,
#          and then refreshed on every poll). POLL_SECONDS is optional (default 120).
#          The data source and skillset named in the indexer must already exist.
# Tools:   curl, jq, az (only when TOKEN is unset).
# Exit:    0 caught up (two runs in a row processed 0 items), 1 a check or run failed,
#          2 bad usage or settings.
set -euo pipefail

index_file="${1:-}"
indexer_file="${2:-}"
if [ -z "${index_file}" ] || [ -z "${indexer_file}" ] || [ ! -f "${index_file}" ] || [ ! -f "${indexer_file}" ]; then
  echo "usage: reindex.sh <index-definition.json> <indexer-definition.json>" >&2
  exit 2
fi
for var in SEARCH_ENDPOINT API_VERSION ALIAS; do
  if [ -z "${!var:-}" ]; then
    echo "error: set ${var} first" >&2
    exit 2
  fi
done
own_token="no"
if [ -z "${TOKEN:-}" ]; then
  own_token="yes"
fi
refresh_token() {
  if [ "${own_token}" = "yes" ]; then
    TOKEN="$(az account get-access-token --scope https://search.azure.com/.default --query accessToken --output tsv)"
  fi
}
refresh_token
poll_seconds="${POLL_SECONDS:-120}"
body="$(mktemp)"
trap 'rm -f "${body}"' EXIT

# call METHOD PATH [FILE]: writes the response body to ${body}, prints the HTTP status code.
call() {
  local method="$1" path="$2" file="${3:-}"
  local args=(-sS -o "${body}" -w "%{http_code}" -X "${method}"
    -H "Authorization: Bearer ${TOKEN}"
    "${SEARCH_ENDPOINT}${path}?api-version=${API_VERSION}")
  if [ -n "${file}" ]; then
    args+=(-H "Content-Type: application/json" -H "Prefer: return=representation" --data "@${file}")
  fi
  : > "${body}"
  curl "${args[@]}" || true
}
stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }

index="$(jq -r '.name' "${index_file}")"
indexer="$(jq -r '.name' "${indexer_file}")"
target="$(jq -r '.targetIndexName' "${indexer_file}")"
datasource="$(jq -r '.dataSourceName' "${indexer_file}")"
skillset="$(jq -r '.skillsetName // empty' "${indexer_file}")"

# 1. Safety checks: the indexer targets the new index, not the alias, and the alias is not on it.
if [ "${target}" != "${index}" ] || [ "${target}" = "${ALIAS}" ]; then
  echo "FAIL: indexer targetIndexName (${target}) must be the new index (${index}), never the alias." >&2
  exit 1
fi
code="$(call GET "/aliases/${ALIAS}")"
if [ "${code}" = "200" ] && [ "$(jq -r '.indexes // [] | join(",")' "${body}")" = "${index}" ]; then
  echo "FAIL: alias ${ALIAS} already points to ${index}. Do not rebuild the live index." >&2
  exit 1
fi
code="$(call GET "/indexes('${index}')")"
if [ "${code}" = "200" ]; then
  echo "FAIL: index ${index} already exists. Choose a new name or delete it first." >&2
  exit 1
fi
code="$(call GET "/datasources('${datasource}')")"
if [ "${code}" != "200" ]; then
  echo "FAIL: data source ${datasource} not found (HTTP ${code}). Create it first (step 5)." >&2
  exit 1
fi
if [ -n "${skillset}" ]; then
  code="$(call GET "/skillsets('${skillset}')")"
  if [ "${code}" != "200" ]; then
    echo "FAIL: skillset ${skillset} not found (HTTP ${code}). Create it first (step 5)." >&2
    exit 1
  fi
fi

# 2. Create the index, then the indexer in a disabled state.
code="$(call PUT "/indexes('${index}')" "${index_file}")"
if [ "${code}" != "201" ] && [ "${code}" != "200" ]; then
  echo "FAIL: create index returned HTTP ${code}: $(cat "${body}")" >&2
  exit 1
fi
echo "$(stamp) index ${index} created (HTTP ${code})."
disabled_file="$(mktemp)"
enabled_file="$(mktemp)"
trap 'rm -f "${body}" "${disabled_file}" "${enabled_file}"' EXIT
jq '.disabled = true' "${indexer_file}" > "${disabled_file}"
jq '.disabled = false' "${indexer_file}" > "${enabled_file}"
code="$(call PUT "/indexers('${indexer}')" "${disabled_file}")"
if [ "${code}" != "201" ] && [ "${code}" != "200" ]; then
  echo "FAIL: create indexer returned HTTP ${code}: $(cat "${body}")" >&2
  exit 1
fi
echo "$(stamp) indexer ${indexer} created disabled (HTTP ${code})."

# 3. Enable the indexer and start the first run. The schedule starts it anyway if Run is refused.
code="$(call PUT "/indexers('${indexer}')" "${enabled_file}")"
if [ "${code}" != "200" ] && [ "${code}" != "201" ]; then
  echo "FAIL: enabling the indexer returned HTTP ${code}: $(cat "${body}")" >&2
  exit 1
fi
code="$(call POST "/indexers('${indexer}')/search.run")"
echo "$(stamp) run requested (HTTP ${code}; 202 expected, otherwise the schedule starts it)."

# 4. Poll until two runs in a row succeed with 0 items processed.
transient=0
while true; do
  sleep "${poll_seconds}"
  refresh_token
  code="$(call GET "/indexers('${indexer}')/search.status")"
  if [ "${code}" != "200" ]; then
    echo "$(stamp) status call returned HTTP ${code}; retrying." >&2
    continue
  fi
  jq -c '{status, last: (.lastResult // {} | {status, itemsProcessed, itemsFailed, errorMessage})}' "${body}"
  if [ "$(jq -r '.status' "${body}")" = "error" ]; then
    echo "FAIL: indexer status is error. Read lastResult.errors, fix, then rerun." >&2
    exit 1
  fi
  if [ "$(jq -r '.lastResult.status // ""' "${body}")" = "transientFailure" ]; then
    transient=$((transient + 1))
  else
    transient=0
  fi
  if [ "${transient}" -ge 2 ]; then
    echo "FAIL: two transientFailure results in a row. Page the on-call engineer (section 7)." >&2
    exit 1
  fi
  caught_up="$(jq -r '[.executionHistory[0:2][] | select(.status == "success" and .itemsProcessed == 0)] | length' "${body}")"
  if [ "${caught_up}" = "2" ]; then
    failed="$(jq -r '[.executionHistory[] | .itemsFailed // 0] | add // 0' "${body}")"
    echo "$(stamp) OK: ${indexer} has caught up. Items failed in recent history: ${failed}."
    exit 0
  fi
done
