#!/usr/bin/env bash

:<<'DOC'
HTTP request primitives with retry and status-code classification.

Behavior:
  2xx → succeed, echo response body
  4xx → fail immediately (client error, not retryable)
  5xx → retry with escalating timeouts
  curl error → warn and retry

All functions echo the response body on success and return 1 on failure.
DOC

__http_api__RETRIES=3
__http_api__COOLDOWN_SECS=1
__http_api__TIMEOUT_STEP_SECS=2

:<<'DOC'
Convenience wrapper for GET requests.

Usage:
  http_get "$url"
  http_get "$url" '{"auth":"Bearer x"}'
DOC
function http_get() {
  local url="$1" options="${2:-{\}}"
  http_request "GET" "$url" "" "$options"
}

:<<'DOC'
Convenience wrapper for POST requests. Sets Content-Type to
application/json unless options.content_type overrides it.

Usage:
  http_post "$url" "$json_body"
  http_post "$url" "$json_body" '{"auth":"Bearer x"}'
DOC
function http_post() {
  local url="$1" body="${2:-}" options="${3:-{\}}"

  local content_type
  content_type="$(jq_get "$options" '.content_type' 'application/json')"
  options="$(jq_set "$options" '.content_type' "$content_type")"

  http_request "POST" "$url" "$body" "$options"
}

:<<'DOC'
Convenience wrapper for PATCH requests. Sets Content-Type to
application/json unless options.content_type overrides it.

Usage:
  http_patch "$url" "$json_body" '{"auth":"token x"}'
DOC
function http_patch() {
  local url="$1" body="${2:-}" options="${3:-{\}}"

  local content_type
  content_type="$(jq_get "$options" '.content_type' 'application/json')"
  options="$(jq_set "$options" '.content_type' "$content_type")"

  http_request "PATCH" "$url" "$body" "$options"
}

:<<'DOC'
Convenience wrapper for DELETE requests.

Usage:
  http_delete "$url" '{"auth":"Bearer x"}'
DOC
function http_delete() {
  local url="$1" options="${2:-{\}}"
  http_request "DELETE" "$url" "" "$options"
}

:<<'DOC'
Uploads a file via POST. Supports two modes via options.upload_mode:

  "form"   (default) — multipart form upload (-F). Field name controlled
           by options.form_field (default "file").
  "binary" — raw body upload (--data-binary). Content-Type set from
           options.content_type (default "application/octet-stream").

Usage:
  http_upload "$url" "$filepath" '{"auth":"Bearer x","upload_mode":"binary"}'
  http_upload "$url" "$filepath" '{"auth":"token x","form_field":"attachment"}'
DOC
function http_upload() {
  local url="$1" filepath="$2" options="${3:-{\}}"

  local auth accept upload_mode
  auth="$(jq_get "$options" '.auth' '')"
  accept="$(jq_get "$options" '.accept' 'application/json')"
  upload_mode="$(jq_get "$options" '.upload_mode' 'form')"

  local -a curl_args=(-s -w '\n%{http_code}' -X POST)
  curl_args+=(-H "Accept: $accept")

  if [[ -n "$auth" ]]; then
    curl_args+=(-H "Authorization: $auth")
  fi

  case "$upload_mode" in
    binary)
      local content_type
      content_type="$(jq_get "$options" '.content_type' 'application/octet-stream')"
      curl_args+=(-H "Content-Type: $content_type")
      curl_args+=(--data-binary "@$filepath")
      ;;
    form)
      local form_field
      form_field="$(jq_get "$options" '.form_field' 'file')"
      curl_args+=(-F "${form_field}=@${filepath}")
      ;;
    *)
      error "http_upload: unknown upload_mode '$upload_mode'"
      return 1
      ;;
  esac

  if ! __http_api__with_retry "$options" __http_api__request_attempt "POST" "$url" "${curl_args[@]}"; then
    error "HTTP POST $url failed after retries"
    return 1
  fi
}

:<<'DOC'
Downloads a file from a URL to a local path with retry on failure.

Unlike http_get, this writes directly to disk via curl -o, making it
safe for binary content (tarballs, images, etc.).

Parameters:
  url       Full download URL
  dest      Local file path to write to
  options   Optional JSON object with any of:
              auth     - Authorization header value
              retries  - Number of attempts. Defaults to 3.
              max_time - Fixed curl --max-time in seconds.

Usage:
  http_download "$url" "/tmp/archive.tar.xz"
  http_download "$url" "$dest" '{"retries":5}'
DOC
function http_download() {
  local url="$1" dest="$2" options="${3:-{\}}"

  local auth
  auth="$(jq_get "$options" '.auth' '')"

  local -a curl_args=(-fSL -o "$dest")

  if [[ -n "$auth" ]]; then
    curl_args+=(-H "Authorization: $auth")
  fi

  if ! __http_api__with_retry "$options" __http_api__download_attempt "$url" "${curl_args[@]}"; then
    rm -f "$dest"
    error "download failed: $url"
    return 1
  fi
}

function __http_api__download_attempt() {
  local attempt="$1" retries="$2" timeout="$3" max_time="$4" url="$5"
  shift 5

  local curl_err
  curl_err="$(mktemp)"

  if curl "$@" --connect-timeout "$timeout" --max-time "$max_time" "$url" 2>"$curl_err"; then
    rm -f "$curl_err"
    return 0
  fi

  if [[ -s "$curl_err" ]]; then
    warn "Attempt $attempt/$retries: $(cat "$curl_err")"
  fi
  rm -f "$curl_err"
  return 1
}

:<<'DOC'
Low-level HTTP request with automatic retry on server errors.

Prefer the method wrappers (http_get, http_post, http_patch, http_delete)
for standard usage. Call http_request directly only for methods that lack
a wrapper (PUT, etc.).

Parameters:
  method   HTTP method (GET, POST, PATCH, DELETE, etc.)
  url      Full request URL
  body     Optional. Request body.
  options  Optional JSON object with any of:
             auth         - Authorization header value (e.g. "token abc", "Bearer abc")
             accept       - Accept header value. Defaults to "application/json".
             content_type - Content-Type header value. Set by http_post/http_patch; omitted if absent.
             max_time     - Fixed curl --max-time in seconds. Overrides escalating default.
             retries      - Number of attempts. Defaults to 3.
DOC
function http_request() {
  local method="$1" url="$2"
  local body="${3:-}" options="${4:-{\}}"

  local auth accept content_type
  auth="$(jq_get "$options" '.auth' '')"
  accept="$(jq_get "$options" '.accept' 'application/json')"
  content_type="$(jq_get "$options" '.content_type' '')"

  local -a curl_args=(-s -w '\n%{http_code}' -X "$method")
  curl_args+=(-H "Accept: $accept")

  if [[ -n "$auth" ]]; then
    curl_args+=(-H "Authorization: $auth")
  fi

  if [[ -n "$content_type" ]]; then
    curl_args+=(-H "Content-Type: $content_type")
  fi

  if [[ -n "$body" ]]; then
    curl_args+=(--data "$body")
  fi

  if ! __http_api__with_retry "$options" __http_api__request_attempt "$method" "$url" "${curl_args[@]}"; then
    error "HTTP $method $url failed after retries"
    return 1
  fi
}

function __http_api__request_attempt() {
  local attempt="$1" retries="$2" timeout="$3" max_time="$4"
  local method="$5" url="$6"
  shift 6

  local raw http_code result curl_err
  curl_err="$(mktemp)"

  raw="$(curl "$@" --connect-timeout "$timeout" --max-time "$max_time" "$url" 2>"$curl_err")" || true
  http_code="$(tail -n1 <<< "$raw")"
  result="$(sed '$d' <<< "$raw")"

  if [[ "$http_code" == 2* ]]; then
    rm -f "$curl_err"
    echo "$result"
    return 0
  fi

  if [[ -s "$curl_err" ]]; then
    warn "Attempt $attempt/$retries: $(cat "$curl_err")"
  fi
  rm -f "$curl_err"

  if [[ "$http_code" != 5* && -n "$http_code" ]]; then
    return 2 
  fi

  return 1
}

: <<'DOC'
Retry loop with escalating timeouts. Delegates each attempt to a callback.

Callback signature:
  callback "$attempt" "$retries" "$timeout" "$max_time" [extra_args...]

Callback return codes:
  0 — success, stop retrying
  1 — retryable failure, try again (e.g. 5xx, network error)
  2 — non-retryable failure, bail immediately (e.g. 4xx)
DOC
function __http_api__with_retry() {
  local options="$1" callback="$2"
  shift 2

  local retries max_time_override
  retries="$(jq_get "$options" '.retries' "$__http_api__RETRIES")"
  max_time_override="$(jq_get "$options" '.max_time' '')"

  for (( attempt = 1; attempt <= retries; attempt++ )); do
    (( attempt > 1 )) && sleep "$__http_api__COOLDOWN_SECS"

    local timeout=$(( __http_api__TIMEOUT_STEP_SECS * attempt ))
    local max_time
    if [[ -n "$max_time_override" ]]; then
      max_time="$max_time_override"
    else
      max_time=$(( timeout * 4 ))
    fi

    local rc=0
    "$callback" "$attempt" "$retries" "$timeout" "$max_time" "$@" || rc=$?

    case $rc in
      0) return 0 ;;
      2) return 1 ;;
      *) ;;
    esac
  done

  return 1
}
