#!/usr/bin/env bash
# Calls the Dorsal HTTP API and prints the JSON response.
#
#   dorsal.sh summaries '{"query":"..."}'     search; the body can also come on stdin
#   dorsal.sh set '{"ids":["..."]}'           full records, at most 25 ids per call
#   dorsal.sh neighbors '{"query":"..."}'     relationships of one named object
#   dorsal.sh protocol dorsal investigate     method for a "dorsal ..." request (or improve, audit)
#   dorsal.sh protocol decision-verification  method for checking a decision
#   dorsal.sh index                           endpoints and body schemas
#
# Needs DORSAL_API_KEY, or the key on the first line of ~/.dorsal/api-key. DORSAL_API_URL overrides the base URL
# (default https://api.dorsal.us).
# The key goes to curl through a file descriptor, so it never appears in a command line or the process list.
# Retries twice on 429 and 503, waiting for Retry-After (at most 30 seconds). Exits 0 only on HTTP 200.
# Results go to stdout and errors to stderr, so an error stays visible when stdout is piped into jq.
set -uo pipefail

usage() {
    printf '%s\n' '{"error":{"type":"usage_error","message":"Usage: dorsal.sh summaries|set|neighbors <json body or stdin> | protocol dorsal <investigate|improve|audit> | protocol decision-verification | index"}}' >&2
    exit 2
}

base="${DORSAL_API_URL:-https://api.dorsal.us}"
base="${base%/}"
key_file="${HOME:-}/.dorsal/api-key"
if [ -z "${DORSAL_API_KEY:-}" ] && [ -n "${HOME:-}" ] && [ -r "$key_file" ]; then
    IFS= read -r DORSAL_API_KEY < "$key_file" || true
    DORSAL_API_KEY="${DORSAL_API_KEY//[[:space:]]/}"
fi
if [ -z "${DORSAL_API_KEY:-}" ]; then
    printf '%s\n' '{"error":{"type":"setup_error","message":"No Dorsal API key. Ask the user to get one at https://godorsal.com/docs/skill and save it in ~/.dorsal/api-key, or export DORSAL_API_KEY. Never ask them to paste the key into the chat."}}' >&2
    exit 2
fi

method=GET
body=""
case "${1:-}" in
    summaries|set|neighbors)
        method=POST
        path="/v1/$1"
        if [ $# -ge 2 ]; then body="$2"; else body="$(cat)"; fi
        [ -n "$body" ] || usage
        ;;
    protocol)
        case "${2:-}" in
            dorsal)
                case "${3:-investigate}" in investigate|improve|audit) ;; *) usage ;; esac
                path="/v1/protocols/dorsal?mode=${3:-investigate}"
                ;;
            decision-verification) path="/v1/protocols/decision-verification" ;;
            *) usage ;;
        esac
        ;;
    index) path="/v1" ;;
    *) usage ;;
esac

out="$(mktemp)"
headers="$(mktemp)"
trap 'rm -f "$out" "$headers"' EXIT

attempt=1
while :; do
    args=(-sS --max-time 60 -o "$out" -D "$headers" -w '%{http_code}')
    # The process substitution must sit on the curl command itself; its descriptor closes after that command.
    if [ "$method" = POST ]; then
        status="$(printf '%s' "$body" | curl "${args[@]}" -K <(printf 'header = "Authorization: Bearer %s"\n' "$DORSAL_API_KEY") -H 'Content-Type: application/json' --data-binary @- "$base$path")" || status="000"
    else
        status="$(curl "${args[@]}" -K <(printf 'header = "Authorization: Bearer %s"\n' "$DORSAL_API_KEY") "$base$path")" || status="000"
    fi
    if { [ "$status" = 429 ] || [ "$status" = 503 ]; } && [ "$attempt" -lt 3 ]; then
        wait="$(awk 'tolower($1) == "retry-after:" { gsub("\r", "", $2); print $2 }' "$headers")"
        case "$wait" in ''|*[!0-9]*) wait=5 ;; esac
        [ "$wait" -gt 30 ] && wait=30
        sleep "$wait"
        attempt=$((attempt + 1))
        continue
    fi
    break
done

if [ "$status" = "000" ]; then
    printf '{"error":{"type":"connection_error","message":"No HTTP response from %s (curl error above). Check DORSAL_API_URL and the network."}}\n' "$base" >&2
    exit 1
fi
if [ "$status" = 200 ]; then
    cat "$out"
    printf '\n'
    exit 0
fi
cat "$out" >&2
printf '\n' >&2
exit 1
