#!/usr/bin/env bash
# aironclaw — minimal CLI wrapper over the AIronClaw REST API.
#
# Reads:
#   AIRONCLAW_TOKEN     personal access token (required)
#   AIRONCLAW_BASE_URL  default: https://dashboard.aironclaw.com
#
# Generic usage:
#   aironclaw <method> <path> [body-json]
#     aironclaw GET /api/mcp
#     aironclaw POST /api/mcp '{"name":"x","url":"https://..."}'
#     aironclaw PATCH /api/llm/<id> '{"defaultModel":"gpt-4o-mini"}'
#     aironclaw DELETE /api/keys/<id>
#
# Convenience subcommands (curated; the generic form covers everything else):
#   aironclaw mcp list
#   aironclaw mcp get <id>
#   aironclaw mcp delete <id>
#   aironclaw mcp tools <id>
#   aironclaw mcp rules <id>
#   aironclaw llm list
#   aironclaw llm get <id>
#   aironclaw llm delete <id>
#   aironclaw llm rules <id>
#   aironclaw keys list
#   aironclaw keys delete <id>
#   aironclaw logs [--limit 50] [--type <event_type>]
#   aironclaw whoami
#
# Security tooling:
#   aironclaw scan run --mcp <id> [--profile standard|aggressive|spec-only|cve-only] [--wait] [--fail-on critical|high|medium]
#   aironclaw scan list [--mcp <id>] [--limit 20]
#   aironclaw scan get <run-id> --mcp <id>
#   aironclaw scan cancel <run-id> --mcp <id>
#   aironclaw scan profile get --mcp <id>
#   aironclaw scan profile set --mcp <id> [--db-family ...] [--runtime ...] [--hosting ...] [--disable-5xx-abort] [--enable-billing-bomb]
#   aironclaw scan feed [--limit 20]
#   aironclaw inspect call --mcp <id> --method <m> [--tool <t>] [--args '<json>'] [--identity self|api-key:<id>|none] [--session <id>]
#   aironclaw inspect cases --mcp <id>
#   aironclaw compliance check --mcp <id>

set -euo pipefail

BASE="${AIRONCLAW_BASE_URL:-https://dashboard.aironclaw.com}"
TOKEN="${AIRONCLAW_TOKEN:-}"

die() { printf 'aironclaw: %s\n' "$*" >&2; exit 1; }

if [[ -z "$TOKEN" ]]; then
  cat >&2 <<'EOF'
aironclaw: AIRONCLAW_TOKEN is not set.

To get one:
  1. Open https://dashboard.aironclaw.com/dashboard/profile
  2. Sign in (and complete 2FA if enabled).
  3. Scroll to "REST API Access" → click "Generate token".
  4. Copy the token shown in the amber banner — it is displayed exactly once.
  5. Export it (and optionally the base URL):

       export AIRONCLAW_TOKEN="<paste-here>"
       export AIRONCLAW_BASE_URL="https://dashboard.aironclaw.com"

Then re-run this command.
EOF
  exit 2
fi

command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
command -v curl >/dev/null 2>&1 || die "curl is required but not installed"

# Low-level request: aironclaw_call <METHOD> <PATH> [body-json]
aironclaw_call() {
  local method="$1" path="$2" body="${3:-}"
  local url="${BASE}${path}"
  local args=(-fsS -X "$method" -H "Authorization: Bearer ${TOKEN}")
  if [[ -n "$body" ]]; then
    args+=(-H 'Content-Type: application/json' -d "$body")
  fi
  # -f makes curl exit non-zero on >=400 but suppresses the body. Re-fetch
  # explicitly on failure so the user sees the error message.
  if ! curl "${args[@]}" "$url" 2>/dev/null; then
    local code
    code=$(curl -sS -o /tmp/aironclaw.err -w '%{http_code}' \
            -X "$method" -H "Authorization: Bearer ${TOKEN}" \
            ${body:+-H 'Content-Type: application/json' -d "$body"} \
            "$url")
    printf '%s %s -> HTTP %s\n%s\n' "$method" "$path" "$code" \
      "$(cat /tmp/aironclaw.err 2>/dev/null || true)" >&2
    return 1
  fi
}

cmd_mcp() {
  local sub="${1:-list}"; shift || true
  case "$sub" in
    list)   aironclaw_call GET "/api/mcp" | jq ;;
    get)    [[ $# -ge 1 ]] || die "mcp get <id>"; aironclaw_call GET "/api/mcp/$1" | jq ;;
    delete) [[ $# -ge 1 ]] || die "mcp delete <id>"; aironclaw_call DELETE "/api/mcp/$1" | jq ;;
    tools)  [[ $# -ge 1 ]] || die "mcp tools <id>"; aironclaw_call POST "/api/mcp/$1/tools" | jq ;;
    rules)  [[ $# -ge 1 ]] || die "mcp rules <id>"; aironclaw_call GET "/api/mcp/$1/rules" | jq ;;
    *) die "unknown mcp subcommand: $sub" ;;
  esac
}

cmd_llm() {
  local sub="${1:-list}"; shift || true
  case "$sub" in
    list)   aironclaw_call GET "/api/llm" | jq ;;
    get)    [[ $# -ge 1 ]] || die "llm get <id>"; aironclaw_call GET "/api/llm/$1" | jq ;;
    delete) [[ $# -ge 1 ]] || die "llm delete <id>"; aironclaw_call DELETE "/api/llm/$1" | jq ;;
    rules)  [[ $# -ge 1 ]] || die "llm rules <id>"; aironclaw_call GET "/api/llm/$1/rules" | jq ;;
    *) die "unknown llm subcommand: $sub" ;;
  esac
}

cmd_keys() {
  local sub="${1:-list}"; shift || true
  case "$sub" in
    list)   aironclaw_call GET "/api/keys" | jq ;;
    delete) [[ $# -ge 1 ]] || die "keys delete <id>"; aironclaw_call DELETE "/api/keys/$1" | jq ;;
    *) die "unknown keys subcommand: $sub" ;;
  esac
}

cmd_logs() {
  local limit=50 type=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --limit) limit="$2"; shift 2 ;;
      --type)  type="$2";  shift 2 ;;
      *) die "unknown logs flag: $1" ;;
    esac
  done
  local q="limit=${limit}"
  [[ -n "$type" ]] && q="${q}&event_type=${type}"
  aironclaw_call GET "/api/logs?${q}" | jq
}

cmd_whoami() {
  aironclaw_call GET "/api/profile" | jq
}

# ── scan ─────────────────────────────────────────────────────────────
#
# High-level orchestration around the scan REST API. Most useful:
#   aironclaw scan run --mcp <id> --wait --fail-on high
# which enqueues, blocks until terminal, prints the summary, and exits
# non-zero if findings ≥ the chosen severity exist.

# Severity rank used by --fail-on comparison (higher = more severe).
__sev_rank() {
  case "$1" in
    critical) printf 5 ;;
    high)     printf 4 ;;
    medium)   printf 3 ;;
    low)      printf 2 ;;
    info)     printf 1 ;;
    *)        printf 0 ;;
  esac
}

cmd_scan() {
  local sub="${1:-list}"; shift || true
  case "$sub" in
    run)     cmd_scan_run     "$@" ;;
    list)    cmd_scan_list    "$@" ;;
    get)     cmd_scan_get     "$@" ;;
    cancel)  cmd_scan_cancel  "$@" ;;
    profile) cmd_scan_profile "$@" ;;
    feed)    cmd_scan_feed    "$@" ;;
    *) die "unknown scan subcommand: $sub (run|list|get|cancel|profile|feed)" ;;
  esac
}

cmd_scan_run() {
  local mcp="" profile="standard" wait_for="" fail_on=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mcp)     mcp="$2"; shift 2 ;;
      --profile) profile="$2"; shift 2 ;;
      --wait)    wait_for=1; shift ;;
      --fail-on) fail_on="$2"; shift 2 ;;
      *) die "unknown scan run flag: $1" ;;
    esac
  done
  [[ -n "$mcp" ]] || die "scan run requires --mcp <id>"

  local body
  body=$(jq -nc --arg p "$profile" '{profile:$p}')
  local resp
  resp=$(aironclaw_call POST "/api/mcp/${mcp}/scan" "$body")
  local run_id
  run_id=$(printf '%s' "$resp" | jq -r '.runId // empty')
  [[ -n "$run_id" ]] || { printf '%s\n' "$resp" >&2; die "enqueue failed"; }
  printf 'enqueued run %s (mcp=%s, profile=%s)\n' "$run_id" "$mcp" "$profile" >&2

  if [[ -z "$wait_for" ]]; then
    printf '%s\n' "$run_id"
    return 0
  fi

  # Poll until terminal.
  local status
  while :; do
    sleep 5
    local row
    row=$(aironclaw_call GET "/api/mcp/${mcp}/scan/${run_id}")
    status=$(printf '%s' "$row" | jq -r '.run.status // "unknown"')
    case "$status" in
      completed|failed|cancelled|aborted) break ;;
    esac
    printf '  status=%s probes=%s reqs=%s findings=%s\n' \
      "$status" \
      "$(printf '%s' "$row" | jq -r '.run.total_probes')" \
      "$(printf '%s' "$row" | jq -r '.run.total_requests')" \
      "$(printf '%s' "$row" | jq -r '.run.findings_count')" >&2
  done

  # Final summary
  local row
  row=$(aironclaw_call GET "/api/mcp/${mcp}/scan/${run_id}")
  printf '%s' "$row" | jq '{
    status: .run.status,
    counters: { probes: .run.total_probes, requests: .run.total_requests, findings: .run.findings_count, duration_s: ((.run.finished_at // 0) - (.run.started_at // 0)) / 1000 },
    by_severity: (.findings | group_by(.severity) | map({(.[0].severity): length}) | add // {}),
    findings: (.findings | sort_by(.severity) | map({severity, title, affected_tool, affected_field, cwe, cve, probe_id}))
  }'

  if [[ -n "$fail_on" ]]; then
    local gate
    gate=$(__sev_rank "$fail_on")
    local hits
    hits=$(printf '%s' "$row" | jq --argjson g "$gate" '
      [.findings[] |
        ( .severity as $s |
          if   $s=="critical" then 5
          elif $s=="high"     then 4
          elif $s=="medium"   then 3
          elif $s=="low"      then 2
          else 1
          end ) |
        select(. >= $g)] | length')
    if [[ "$hits" -gt 0 ]]; then
      printf 'aironclaw: %s finding(s) ≥ %s — exiting non-zero\n' "$hits" "$fail_on" >&2
      exit 1
    fi
  fi
}

cmd_scan_list() {
  local mcp="" limit=20
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mcp)   mcp="$2"; shift 2 ;;
      --limit) limit="$2"; shift 2 ;;
      *) die "unknown scan list flag: $1" ;;
    esac
  done
  if [[ -n "$mcp" ]]; then
    aironclaw_call GET "/api/mcp/${mcp}/scan?limit=${limit}" | jq
  else
    aironclaw_call GET "/api/scan/runs?limit=${limit}" | jq
  fi
}

cmd_scan_get() {
  local run="${1:-}"; shift || true
  [[ -n "$run" ]] || die "scan get <run-id> --mcp <id>"
  local mcp=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mcp) mcp="$2"; shift 2 ;;
      *) die "unknown scan get flag: $1" ;;
    esac
  done
  [[ -n "$mcp" ]] || die "scan get requires --mcp <id>"
  aironclaw_call GET "/api/mcp/${mcp}/scan/${run}" | jq
}

cmd_scan_cancel() {
  local run="${1:-}"; shift || true
  [[ -n "$run" ]] || die "scan cancel <run-id> --mcp <id>"
  local mcp=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mcp) mcp="$2"; shift 2 ;;
      *) die "unknown scan cancel flag: $1" ;;
    esac
  done
  [[ -n "$mcp" ]] || die "scan cancel requires --mcp <id>"
  aironclaw_call DELETE "/api/mcp/${mcp}/scan/${run}" | jq
}

cmd_scan_profile() {
  local sub="${1:-get}"; shift || true
  local mcp=""
  local body=""
  if [[ "$sub" == "get" ]]; then
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --mcp) mcp="$2"; shift 2 ;;
        *) die "unknown scan profile get flag: $1" ;;
      esac
    done
    [[ -n "$mcp" ]] || die "scan profile get requires --mcp <id>"
    aironclaw_call GET "/api/mcp/${mcp}/scan/profile" | jq
    return
  fi
  if [[ "$sub" == "set" ]]; then
    local db_family="" runtime="" hosting=""
    local disable_5xx="" enable_bb=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --mcp)                 mcp="$2"; shift 2 ;;
        --db-family)           db_family="$2"; shift 2 ;;
        --runtime)             runtime="$2"; shift 2 ;;
        --hosting)             hosting="$2"; shift 2 ;;
        --disable-5xx-abort)   disable_5xx=true; shift ;;
        --no-disable-5xx-abort)disable_5xx=false; shift ;;
        --enable-billing-bomb) enable_bb=true; shift ;;
        --no-enable-billing-bomb) enable_bb=false; shift ;;
        *) die "unknown scan profile set flag: $1" ;;
      esac
    done
    [[ -n "$mcp" ]] || die "scan profile set requires --mcp <id>"
    body=$(jq -nc \
      --arg db "$db_family" \
      --arg rt "$runtime" \
      --arg hs "$hosting" \
      --arg d5 "$disable_5xx" \
      --arg eb "$enable_bb" \
      '
      def maybe_str($k; $v): if $v == "" then {} else {($k): $v} end;
      def maybe_bool($k; $v): if $v == "" then {} else {($k): ($v == "true")} end;
      maybe_str("db_family"; $db)
      + maybe_str("runtime"; $rt)
      + maybe_str("hosting"; $hs)
      + maybe_bool("disable_5xx_abort"; $d5)
      + maybe_bool("enable_billing_bomb"; $eb)
      ')
    aironclaw_call PUT "/api/mcp/${mcp}/scan/profile" "$body" | jq
    return
  fi
  die "unknown scan profile subcommand: $sub (get|set)"
}

cmd_scan_feed() {
  local limit=20
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --limit) limit="$2"; shift 2 ;;
      *) die "unknown scan feed flag: $1" ;;
    esac
  done
  aironclaw_call GET "/api/scan/feed?limit=${limit}" | jq
}

# ── inspect ──────────────────────────────────────────────────────────

cmd_inspect() {
  local sub="${1:-call}"; shift || true
  case "$sub" in
    call)  cmd_inspect_call  "$@" ;;
    cases) cmd_inspect_cases "$@" ;;
    *) die "unknown inspect subcommand: $sub (call|cases)" ;;
  esac
}

cmd_inspect_call() {
  local mcp="" method="" tool="" args="" identity="self" session=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mcp)      mcp="$2"; shift 2 ;;
      --method)   method="$2"; shift 2 ;;
      --tool)     tool="$2"; shift 2 ;;
      --args)     args="$2"; shift 2 ;;
      --identity) identity="$2"; shift 2 ;;
      --session)  session="$2"; shift 2 ;;
      *) die "unknown inspect call flag: $1" ;;
    esac
  done
  [[ -n "$mcp" ]] || die "inspect call requires --mcp <id>"
  [[ -n "$method" ]] || die "inspect call requires --method <m>"

  local params
  if [[ -n "$tool" ]]; then
    local arg_obj="${args:-{\}}"
    params=$(jq -nc --arg n "$tool" --argjson a "$arg_obj" '{name:$n, arguments:$a}')
  elif [[ -n "$args" ]]; then
    params="$args"
  else
    params='{}'
  fi

  local body
  body=$(jq -nc \
    --arg m "$method" \
    --argjson p "$params" \
    --arg i "$identity" \
    --arg s "$session" \
    '
    {method:$m, params:$p, identity:$i}
    + (if $s == "" then {} else {sessionId:$s} end)
    ')
  aironclaw_call POST "/api/mcp/${mcp}/inspect" "$body" | jq
}

cmd_inspect_cases() {
  local mcp=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mcp) mcp="$2"; shift 2 ;;
      *) die "unknown inspect cases flag: $1" ;;
    esac
  done
  [[ -n "$mcp" ]] || die "inspect cases requires --mcp <id>"
  aironclaw_call GET "/api/mcp/${mcp}/inspect/cases" | jq
}

# ── compliance ───────────────────────────────────────────────────────

cmd_compliance() {
  local sub="${1:-check}"; shift || true
  case "$sub" in
    check) cmd_compliance_check "$@" ;;
    *) die "unknown compliance subcommand: $sub (check)" ;;
  esac
}

cmd_compliance_check() {
  local mcp=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mcp) mcp="$2"; shift 2 ;;
      *) die "unknown compliance check flag: $1" ;;
    esac
  done
  [[ -n "$mcp" ]] || die "compliance check requires --mcp <id>"
  local resp
  resp=$(aironclaw_call POST "/api/mcp/${mcp}/inspect/compliance")
  printf '%s' "$resp" | jq '{
    summary,
    results: [.results[] | {id, label, status, message, rationale, latencyMs}]
  }'
  # Exit non-zero if any probe failed — convenient as a CI gate.
  local fails
  fails=$(printf '%s' "$resp" | jq '.summary.fail')
  if [[ "$fails" -gt 0 ]]; then
    printf 'aironclaw: %s compliance probe(s) failed\n' "$fails" >&2
    exit 1
  fi
}

main() {
  if [[ $# -eq 0 ]]; then
    sed -n '3,42p' "$0" | sed 's/^# \{0,1\}//'
    exit 0
  fi

  local first="$1"; shift
  case "$first" in
    GET|POST|PUT|PATCH|DELETE)
      [[ $# -ge 1 ]] || die "missing path after $first"
      local path="$1"; shift
      local body="${1:-}"
      aironclaw_call "$first" "$path" "$body" | jq
      ;;
    mcp)        cmd_mcp        "$@" ;;
    llm)        cmd_llm        "$@" ;;
    keys)       cmd_keys       "$@" ;;
    logs)       cmd_logs       "$@" ;;
    whoami)     cmd_whoami           ;;
    scan)       cmd_scan       "$@" ;;
    inspect)    cmd_inspect    "$@" ;;
    compliance) cmd_compliance "$@" ;;
    -h|--help|help) sed -n '3,42p' "$0" | sed 's/^# \{0,1\}//' ;;
    *) die "unknown command: $first (use --help)" ;;
  esac
}

main "$@"
