#!/usr/bin/env bash

:<<'DOC'
Cursor Cloud Agents API client for launching and polling headless agents.
Built on http_request from http.api.sh.

API reference: https://cursor.com/docs-static/cloud-agents-openapi.yaml
DOC

import "$SHELLOPS_DIR/lib/http.api.sh"

__cursor_api__BASE="https://api.cursor.com"
__cursor_api__AGENTS_ENDPOINT="/v0/agents"

__cursor_api__POLL_INTERVAL_SECS=10
__cursor_api__POLL_MAX_ATTEMPTS=120

:<<'DOC'
Creates default Cursor API options.

Usage: new_cursor_options
DOC
function new_cursor_options() {
  jq_set '{}' \
    '.api_key'    "" \
    '.repository' "" \
    '.ref'        "main" \
    '.model'      ""
}

function __cursor_api__validate_key() {
  local options="$1"
  local key

  key="$(jq_get "$options" '.api_key')"
  if [[ -z "$key" ]]; then
    error "No Cursor API key set"
    return 1
  fi

  echo "$key"
}

function __cursor_api__agent_url() {
  local agent_id="$1"
  echo "${__cursor_api__BASE}${__cursor_api__AGENTS_ENDPOINT}/${agent_id}"
}

function __cursor_api__http_opts() {
  jq_set '{}' '.auth' "Bearer $1"
}

function __cursor_api__launch_http_opts() {
  jq_set '{}' '.auth' "Bearer $1" '.max_time' 30 '.retries' 1
}

:<<'DOC'
Launches a cloud agent with the given prompt text.

When options.model is non-empty, sets the top-level "model" field on the
create request (explicit model id). When empty, the field is omitted and
Cursor resolves user/team/system defaults.

Returns the agent ID on success.

Usage: cursor_launch_agent "$options" "$prompt_text"
DOC
function cursor_launch_agent() {
  local options="$1" prompt_text="$2"
  local api_key repository ref model body result http_opts

  api_key="$(__cursor_api__validate_key "$options")" || return 1
  http_opts="$(__cursor_api__launch_http_opts "$api_key")"
  repository="$(jq_get "$options" '.repository')"
  ref="$(jq_get "$options" '.ref' 'main')"
  model="$(jq_get "$options" '.model' '')"

  body="$(jq_set '{}' \
    '.prompt.text'          "$prompt_text" \
    '.source.repository'    "$repository" \
    '.source.ref'           "$ref" \
    '.target.autoCreatePr'  false)"

  if [[ -n "$model" ]]; then
    body="$(jq_set "$body" '.model' "$model")"
  fi

  result="$(
    http_post \
      "${__cursor_api__BASE}${__cursor_api__AGENTS_ENDPOINT}" \
      "$body" \
      "$http_opts"
  )" || return 1

  jq_get "$result" '.id'
}

:<<'DOC'
Lists model IDs recommended for agent launch requests (GET /v0/models).

Returns a JSON string array of model id strings.

Usage: cursor_list_models "$options"
DOC
function cursor_list_models() {
  local options="$1"
  local api_key result http_opts

  api_key="$(__cursor_api__validate_key "$options")" || return 1
  http_opts="$(__cursor_api__http_opts "$api_key")"

  result="$(
    http_get \
      "${__cursor_api__BASE}/v0/models" \
      "$http_opts"
  )" || return 1

  jq_get "$result" '.models'
}

:<<'DOC'
Polls a cloud agent until it reaches a terminal status.

Terminal statuses: FINISHED, ERROR, EXPIRED.
Returns the final status string.

Usage: cursor_poll_agent "$options" "$agent_id"
DOC
function cursor_poll_agent() {
  local options="$1" agent_id="$2"
  local api_key url status result http_opts

  api_key="$(__cursor_api__validate_key "$options")" || return 1
  http_opts="$(__cursor_api__http_opts "$api_key")"
  url="$(__cursor_api__agent_url "$agent_id")"

  for (( attempt = 1; attempt <= __cursor_api__POLL_MAX_ATTEMPTS; attempt++ )); do
    result="$(http_get "$url" "$http_opts")" || return 1
    status="$(jq_get "$result" '.status')"

    case "$status" in
      FINISHED|ERROR|EXPIRED)
        echo "$status"
        return 0
        ;;
      RUNNING|CREATING)
        log "Agent $agent_id: $status (poll $attempt/$__cursor_api__POLL_MAX_ATTEMPTS)"
        sleep "$__cursor_api__POLL_INTERVAL_SECS"
        ;;
      *)
        error "Agent $agent_id: unexpected status '$status'"
        return 1
        ;;
    esac
  done

  error "Agent $agent_id: timed out after $__cursor_api__POLL_MAX_ATTEMPTS polls"
  return 1
}

:<<'DOC'
Retrieves the agent's conversation and extracts the last assistant
message's text content.

Usage: cursor_get_result "$options" "$agent_id"
DOC
function cursor_get_result() {
  local options="$1" agent_id="$2"
  local api_key url result messages http_opts
  local assistant_messages last_message

  api_key="$(__cursor_api__validate_key "$options")" || return 1
  http_opts="$(__cursor_api__http_opts "$api_key")"
  url="$(__cursor_api__agent_url "$agent_id")/conversation"

  result="$(http_get "$url" "$http_opts")" || return 1
  messages="$(jq_get "$result" '.messages')"

  assistant_messages="$(jq_filter "$messages" '.type == "assistant_message"')"
  last_message="$(jq_last "$assistant_messages")"
  jq_get "$last_message" '.text'
}

:<<'DOC'
Sends a followup prompt to an existing agent and polls until it finishes.

Returns the agent's latest assistant message text on success.

Usage: cursor_followup_agent "$options" "$agent_id" "$followup_text"
DOC
function cursor_followup_agent() {
  local options="$1" agent_id="$2" followup_text="$3"
  local api_key url body status http_opts

  api_key="$(__cursor_api__validate_key "$options")" || return 1
  http_opts="$(__cursor_api__http_opts "$api_key")"
  url="$(__cursor_api__agent_url "$agent_id")/followup"
  body="$(jq_set '{}' '.prompt.text' "$followup_text")"

  http_post "$url" "$body" "$http_opts" > /dev/null || return 1
  log "Followup sent to agent $agent_id"

  status="$(cursor_poll_agent "$options" "$agent_id")" || return 1
  if [[ "$status" != "FINISHED" ]]; then
    error "Agent $agent_id followup ended with status: $status"
    return 1
  fi

  cursor_get_result "$options" "$agent_id"
}

:<<'DOC'
Deletes a cloud agent (HTTP DELETE on the agent resource). Permanent and
cannot be undone.

The [Cursor Cloud Agents OpenAPI](https://cursor.com/docs-static/cloud-agents-openapi.yaml)
documents this as deleting the agent resource; in practice the provider
stops server-side work tied to that id, so calling this while status is
still RUNNING/CREATING is the right way to tear down an agent when a CI job
is cancelled—e.g. `trap 'cursor_cleanup_agent "$opts" "$agent_id"' EXIT`
after launch, or a dedicated cleanup step that reads the last agent id from
job state.

Use after you have fetched the result you need (e.g. FINISHED) or when
discarding a failed run so the agent does not linger in the Cursor
dashboard. For CI and headless pipelines this is the appropriate cleanup
once polling ends; do not DELETE an agent you still need to poll or read
conversation from.

Usage: cursor_cleanup_agent "$options" "$agent_id"
DOC
function cursor_cleanup_agent() {
  local options="$1" agent_id="$2"
  local api_key url http_opts

  api_key="$(__cursor_api__validate_key "$options")" || return 1
  http_opts="$(__cursor_api__http_opts "$api_key")"
  url="$(__cursor_api__agent_url "$agent_id")"

  http_delete "$url" "$http_opts" > /dev/null
}
