#!/usr/bin/env bash
# Author: Marcel Herrguth, Claude Code
set -euo pipefail

usage() {
  cat <<EOF
Usage: $0 -u <url> -U <user> -p <password> [-k]

  -u  Elasticsearch URL (default: https://127.0.0.1:9200)
  -U  Username          (default: elastic)
  -p  Password          (required, or set ES_PASSWORD env var)
  -k  Allow insecure TLS (skip cert verification)
EOF
  exit 1
}

url="https://127.0.0.1:9200"
user="elastic"
password="${ES_PASSWORD:-}"
insecure=()

while getopts "u:U:p:kh" opt; do
  case "$opt" in
    u) url="$OPTARG" ;;
    U) user="$OPTARG" ;;
    p) password="$OPTARG" ;;
    k) insecure=(--insecure) ;;
    h|*) usage ;;
  esac
done

if [[ -z "$password" ]]; then
  echo "Password required (-p or ES_PASSWORD)"
  usage
fi

CURL=(curl "${insecure[@]}" -s -u "${user}:${password}")

fetch_settings_mappings() {
  # $1 = index to read from
  settings=$("${CURL[@]}" "${url}/$1/_settings" | jq ".[\"$1\"].settings.index | del(.uuid, .creation_date, .version, .provided_name, .resize, .blocks)")
  mappings=$("${CURL[@]}" "${url}/$1/_mapping" | jq ".[\"$1\"].mappings")
}

create_index() {
  # $1 = index to create, uses $settings/$mappings from fetch_settings_mappings
  body=$(jq -n --argjson settings "$settings" --argjson mappings "$mappings" '{settings: {index: $settings}, mappings: $mappings}')
  "${CURL[@]}" -X PUT "${url}/$1" -H 'Content-Type: application/json' -d "$body" >/dev/null
}

reindex() {
  # $1 = source, $2 = dest; prints the summary line, sets $reindex_ok
  result=$("${CURL[@]}" -X POST "${url}/_reindex" -H 'Content-Type: application/json' -d "{\"source\":{\"index\":\"$1\"},\"dest\":{\"index\":\"$2\"}}")
  echo "$result" | jq -c '{took, total, created, failures: (.failures | length)}'

  failure_count=$(echo "$result" | jq '(.failures // []) | length')
  has_error=$(echo "$result" | jq 'has("error")')
  if [[ "$failure_count" != "0" || "$has_error" == "true" ]]; then
    reindex_ok=false
  else
    reindex_ok=true
  fi
}

indices=$("${CURL[@]}" "${url}/_migration/deprecations" | jq -r '(.index_settings // {}) | to_entries[] | select(.value[]?._meta.reindex_required == true) | .key')

failed=()

if [[ -z "$indices" ]]; then
  echo "No plain indices require reindexing."
else
  indices_csv=$(echo "$indices" | paste -sd, -)
  sizes=$("${CURL[@]}" "${url}/_cat/indices/${indices_csv}?h=index,store.size&bytes=b")

  echo "Indices to rebuild in place:"
  total_bytes=0
  max_bytes=0
  while read -r name bytes; do
    [[ -n "$name" ]] || continue
    human=$(numfmt --to=iec --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B")
    printf '  - %-60s %s\n' "$name" "$human"
    total_bytes=$(( total_bytes + bytes ))
    (( bytes > max_bytes )) && max_bytes=$bytes
  done <<< "$sizes"

  echo
  echo "Total on-disk size across all listed indices: $(numfmt --to=iec --suffix=B "$total_bytes")"
  echo "Each index is rebuilt one at a time, so the extra headroom you actually need is roughly the size"
  echo "of the single largest index below (briefly held twice during its own rebuild), not the sum of all:"
  echo "  largest single index: $(numfmt --to=iec --suffix=B "$max_bytes")"
  echo

  read -rp "Proceed with index rebuild? Each index is rebuilt under its original name (no aliases left behind). [y/N] " ok
  if [[ "$ok" == "y" ]]; then
    for idx in $indices; do
      tmp="${idx}-tmp-reindex"
      echo "=== ${idx}: rebuilding via ${tmp} ==="

      fetch_settings_mappings "$idx"
      "${CURL[@]}" -X PUT "${url}/${idx}/_settings" -H 'Content-Type: application/json' -d '{"index.blocks.write": true}' >/dev/null

      create_index "$tmp"
      reindex "$idx" "$tmp"
      if [[ "$reindex_ok" != "true" ]]; then
        echo "!!! first-hop reindex failed for ${idx} — leaving original untouched, removing ${tmp}"
        "${CURL[@]}" -X DELETE "${url}/${tmp}" >/dev/null
        "${CURL[@]}" -X PUT "${url}/${idx}/_settings" -H 'Content-Type: application/json' -d '{"index.blocks.write": false}' >/dev/null
        failed+=("$idx (first hop)")
        continue
      fi

      "${CURL[@]}" -X DELETE "${url}/${idx}" >/dev/null
      create_index "$idx"
      reindex "$tmp" "$idx"
      if [[ "$reindex_ok" != "true" ]]; then
        echo "!!! second-hop reindex failed for ${idx} — data is safe in ${tmp}, NOT deleting it. ${idx} may be partially populated."
        failed+=("$idx (second hop — recover from ${tmp})")
        continue
      fi

      "${CURL[@]}" -X DELETE "${url}/${tmp}" >/dev/null
      echo "=== done: ${idx} rebuilt (real index, no alias) ==="
    done
  else
    echo "Skipping index rebuild."
  fi
fi

migrate_data_stream() {
  local ds="$1"
  echo "=== data stream: ${ds} ==="

  start_resp=$("${CURL[@]}" -X POST "${url}/_migration/reindex" -H 'Content-Type: application/json' -d "{\"source\":{\"index\":\"${ds}\"},\"mode\":\"upgrade\"}")
  if echo "$start_resp" | jq -e 'has("error")' >/dev/null; then
    echo "!!! failed to start migration for data stream ${ds}:"
    echo "$start_resp" | jq -c .
    ds_failed+=("$ds")
    return
  fi

  while true; do
    status=$("${CURL[@]}" "${url}/_migration/reindex/${ds}/_status")
    complete=$(echo "$status" | jq -r '.complete // false')
    successes=$(echo "$status" | jq -r '.successes // 0')
    total=$(echo "$status" | jq -r '.total_indices_requiring_upgrade // 0')
    pending=$(echo "$status" | jq -r '.pending // 0')
    echo "  progress: ${successes}/${total} backing indices upgraded, ${pending} pending"
    [[ "$complete" == "true" ]] && break
    sleep 5
  done

  err_count=$(echo "$status" | jq '(.errors // []) | length')
  if [[ "$err_count" != "0" ]]; then
    echo "!!! data stream ${ds} finished with errors:"
    echo "$status" | jq -c '.errors'
    ds_failed+=("$ds")
  else
    echo "=== done: ${ds} (${successes}/${total} backing indices upgraded, history preserved) ==="
  fi
}

data_streams=$("${CURL[@]}" "${url}/_migration/deprecations" | jq -r '(.data_streams // {}) | to_entries[] | select(.value[]?._meta.reindex_required == true) | .key')

ds_failed=()

if [[ -n "$data_streams" ]]; then
  backing_indices=$("${CURL[@]}" "${url}/_migration/deprecations" | jq -r '(.data_streams // {}) | to_entries[] | select(.value[]?._meta.reindex_required == true) | .value[]._meta.indices_requiring_upgrade[]')
  backing_csv=$(echo "$backing_indices" | paste -sd, -)
  ds_sizes=$("${CURL[@]}" "${url}/_cat/indices/${backing_csv}?h=index,store.size&bytes=b")

  echo
  echo "Data streams to migrate (native _migration/reindex, no data discarded):"
  ds_total_bytes=0
  ds_max_bytes=0
  while read -r name bytes; do
    [[ -n "$name" ]] || continue
    human=$(numfmt --to=iec --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B")
    printf '  - %-60s %s\n' "$name" "$human"
    ds_total_bytes=$(( ds_total_bytes + bytes ))
    (( bytes > ds_max_bytes )) && ds_max_bytes=$bytes
  done <<< "$ds_sizes"
  echo "  ($(echo "$data_streams" | wc -l | tr -d ' ') data stream(s), backing indices totaling $(numfmt --to=iec --suffix=B "$ds_total_bytes"), largest single backing index $(numfmt --to=iec --suffix=B "$ds_max_bytes"))"
  echo

  read -rp "Migrate these data streams in place? [y/N] " ds_ok
  if [[ "$ds_ok" == "y" ]]; then
    for ds in $data_streams; do
      migrate_data_stream "$ds"
    done
  else
    echo "Skipping data stream migration."
  fi
fi

sf_failed=()

before_sf=$("${CURL[@]}" "${url}/_migration/system_features")
sf_status=$(echo "$before_sf" | jq -r '.migration_status')

if [[ "$sf_status" == "NO_MIGRATION_NEEDED" ]]; then
  echo
  echo "No system feature migration needed."
else
  echo
  echo "System features requiring migration:"
  echo "$before_sf" | jq -r '.features[] | select(.migration_status=="MIGRATION_NEEDED") | "  - \(.feature_name): \(.indices | map(.index) | join(", "))"'
  echo

  read -rp "Trigger system feature migration (POST /_migration/system_features)? [y/N] " sf_ok
  if [[ "$sf_ok" == "y" ]]; then
    before_map=$(echo "$before_sf" | jq -c '[.features[] | {feature: .feature_name, indices: [.indices[].index]}]')

    post_resp=$("${CURL[@]}" -X POST "${url}/_migration/system_features")
    echo "$post_resp" | jq -c .

    if echo "$post_resp" | jq -e '.accepted == false' >/dev/null; then
      echo "!!! system feature migration was not accepted"
      sf_failed+=("system_features (not accepted)")
    else
      after_sf=""
      while true; do
        after_sf=$("${CURL[@]}" "${url}/_migration/system_features")
        top=$(echo "$after_sf" | jq -r '.migration_status')
        echo "  status: ${top}"
        if [[ "$top" == "ERROR" ]]; then
          echo "!!! system feature migration reported ERROR:"
          echo "$after_sf" | jq -c '.features[] | select(.migration_status=="ERROR")'
          sf_failed+=("system_features (error)")
          break
        fi
        [[ "$top" == "NO_MIGRATION_NEEDED" ]] && break
        sleep 5
      done

      if [[ "$top" == "NO_MIGRATION_NEEDED" ]]; then
        after_map=$(echo "$after_sf" | jq -c '[.features[] | {feature: .feature_name, indices: [.indices[].index]}]')

        orphans=$(jq -n --argjson before "$before_map" --argjson after "$after_map" '
          ($after | map({(.feature): .indices}) | add) as $afterByFeature |
          [ $before[] | . as $b |
            ($afterByFeature[$b.feature] // []) as $a |
            select(($a | length) > 0) |
            ($b.indices - $a)[]
          ]')

        echo "Old pre-migration indices confirmed superseded (locking read-only, not deleting):"
        for old in $(echo "$orphans" | jq -r '.[]'); do
          exists=$("${CURL[@]}" -o /dev/null -w '%{http_code}' "${url}/${old}")
          if [[ "$exists" == "200" ]]; then
            "${CURL[@]}" -X PUT "${url}/${old}/_settings" -H 'Content-Type: application/json' -d '{"index.blocks.write": true}' >/dev/null
            echo "  - ${old}: locked"
          fi
        done
      fi
    fi
  else
    echo "Skipping system feature migration."
  fi
fi

if (( ${#failed[@]} > 0 || ${#ds_failed[@]} > 0 || ${#sf_failed[@]} > 0 )); then
  echo
  echo "Finished with failures:"
  printf '  - %s\n' "${failed[@]}" "${ds_failed[@]}" "${sf_failed[@]}"
  exit 1
fi
