#!/bin/bash
# Indecs -- process a single file
# https://indecs.app
#
# Usage:
#   INDECS_API_KEY="idk_your-key-here" bash <(curl -sfL --connect-timeout 5 https://indecs.app/scan.sh) /path/to/file.pdf
#
# Or download first and inspect before running (recommended if you'd rather
# not pipe a remote script straight into bash):
#   curl -sfL https://indecs.app/scan.sh -o scan.sh && chmod +x scan.sh
#   INDECS_API_KEY="idk_your-key-here" ./scan.sh /path/to/file.pdf
#
# Hazel rule ("Run shell script", inline):
#   INDECS_API_KEY="idk_your-key-here" bash <(curl -sfL --connect-timeout 5 https://indecs.app/scan.sh) "$1" \
#     || osascript -e 'display notification "Could not reach Indecs." with title "Indecs" subtitle "Offline"'
#
# Two things worth knowing before you run this:
#   - INDECS_API_KEY on the command line lands in your shell history and, in a
#     Hazel rule, in plain text in Hazel's own config. If that bothers you,
#     put the key in a ~/.indecs file (KEY=value, one per line) and source it
#     from your own wrapper script instead of passing it inline.
#   - `bash <(curl ...)` runs whatever https://indecs.app is serving at the
#     moment you run it. That's a reasonable thing to offer and a reasonable
#     thing for a careful user to decline -- use the download-then-run form
#     above if you'd rather read the script first.
#
# Dependencies: curl, jq (brew install jq), optionally tag (brew install tag)

set -uo pipefail

# ── Configuration (override via environment) ──────────────────
API_KEY="${INDECS_API_KEY:-}"
BASE_URL="${INDECS_URL:-https://indecs.app}"
POLL_INTERVAL="${INDECS_POLL_INTERVAL:-3}"
MAX_POLLS="${INDECS_MAX_POLLS:-60}"
RENAME_FILE="${INDECS_RENAME:-false}"
ADD_TAGS="${INDECS_TAGS:-true}"
NOTIFY="${INDECS_NOTIFY:-false}"
CONNECT_TIMEOUT=5
DEBUG="${INDECS_DEBUG:-false}"
# ───────────────────────────────────────────────────────────────

debug() { [ "$DEBUG" = "true" ] && echo "[scan.sh] $*" >&2; true; }

if [ -z "$API_KEY" ]; then
  echo "Set INDECS_API_KEY in your Hazel script." >&2
  exit 1
fi

# ${1:-} rather than $1: with `set -u` a missing argument aborts with an opaque
# "unbound variable" instead of the clear message below. In Hazel the argument
# is "$1"; running the one-liner by hand without a file is the usual way to hit
# this.
FILE="${1:-}"
if [ -z "$FILE" ]; then
  echo "Usage: scan.sh <file>  (no file argument was passed)" >&2
  exit 1
fi
debug "FILE=$FILE"
debug "RENAME_FILE=$RENAME_FILE ADD_TAGS=$ADD_TAGS NOTIFY=$NOTIFY"

if [ ! -f "$FILE" ]; then
  echo "File not found: $FILE" >&2
  exit 1
fi

# Skip if already processed (Hazel re-triggers when we modify the file)
if xattr -p com.indecs.processed "$FILE" &>/dev/null; then
  debug "Already processed (xattr set), skipping"
  exit 0
fi

notify() {
  # Always surface the message on stderr so a refusal is visible in a terminal
  # and in Hazel's log even when INDECS_NOTIFY is off (the default). The macOS
  # notification is the opt-in extra, not the only channel -- routing failures
  # solely through it is what made paused/rejected uploads look like silence.
  echo "[indecs] $1: $2" >&2
  [ "$NOTIFY" = "true" ] && osascript -e "display notification \"$2\" with title \"Indecs\" subtitle \"$1\""
  true
}

# Helper: curl with connection timeout, returns "" on network failure
api() {
  curl -sf --connect-timeout "$CONNECT_TIMEOUT" "$@" 2>/dev/null || true
}

# ── Check credits ──────────────────────────────────────────────
debug "Checking credits..."
CREDITS=$(api -H "Authorization: Bearer $API_KEY" "$BASE_URL/api/credits")
debug "CREDITS response: $CREDITS"

if [ -z "$CREDITS" ]; then
  notify "Offline" "$(basename "$FILE") will be processed when you're back online."
  exit 0
fi

TOTAL=$(echo "$CREDITS" | jq -r '.total')

if [ "$TOTAL" = "0" ]; then
  notify "Out of credits" "$(basename "$FILE") was not processed. Top up your Indecs plan to continue."
  exit 0
fi

# ── Upload ─────────────────────────────────────────────────────
debug "Uploading $(basename "$FILE")..."
# Deliberately not the `api` helper: that uses curl -sf, which discards the
# body on any 4xx. The server explains itself in that body (no_credits,
# sender_paused), so swallowing it turned every refusal into an empty response
# and an "Offline" notification, which is exactly the case where a client
# should stop rather than retry.
UPLOAD=$(curl -s --connect-timeout "$CONNECT_TIMEOUT" -X POST \
  -H "Authorization: Bearer $API_KEY" \
  -F "files=@$FILE" \
  "$BASE_URL/api/upload?mode=metadata" 2>/dev/null || true)
debug "UPLOAD response: $UPLOAD"

if [ -z "$UPLOAD" ]; then
  notify "Offline" "Could not upload $(basename "$FILE"). Check your connection."
  exit 0
fi

OK=$(echo "$UPLOAD" | jq -r '.ok // empty')

if [ "$OK" != "true" ]; then
  ERROR=$(echo "$UPLOAD" | jq -r '.error // "Upload failed"')
  MESSAGE=$(echo "$UPLOAD" | jq -r '.message // empty')
  if [ "$ERROR" = "no_credits" ]; then
    notify "Out of credits" "$(basename "$FILE") was not processed. Top up your Indecs plan."
    exit 0
  fi
  # Uploads are paused account-wide after repeated rejections. Retrying is the
  # behaviour that caused the pause, and it applies to every file rather than
  # this one, so exit 3: batch-scan.sh stops the whole run instead of working
  # through the remaining files collecting 403s.
  if [ "$ERROR" = "sender_paused" ]; then
    RETRY_AFTER=$(echo "$UPLOAD" | jq -r '.retry_after // empty')
    notify "Uploads paused" "${MESSAGE:-Uploads are paused after repeated rejected documents.}${RETRY_AFTER:+ Resumes $RETRY_AFTER.}"
    exit 3
  fi
  notify "Upload failed" "${MESSAGE:-$ERROR}"
  exit 1
fi

JOB_ID=$(echo "$UPLOAD" | jq -r '.job_ids[0]')
debug "JOB_ID=$JOB_ID"

# ── Poll for result ────────────────────────────────────────────
# api_jobs/* is rate limited per API key (see rate-limit.ts). A 429 there
# means the batch job is running hot, not that this particular job stalled --
# back off for Retry-After seconds and retry without counting it against
# MAX_POLLS, so a rate-limited run still gets its full poll budget.
poll_job() {
  local headers_file body status
  headers_file=$(mktemp)
  body=$(curl -s --connect-timeout "$CONNECT_TIMEOUT" -D "$headers_file" \
    -H "Authorization: Bearer $API_KEY" "$BASE_URL/api/jobs/$JOB_ID" 2>/dev/null)
  status=$(head -1 "$headers_file" 2>/dev/null | awk '{print $2}')
  retry_after=$(grep -i '^Retry-After:' "$headers_file" 2>/dev/null | tr -d '\r' | awk '{print $2}')
  rm -f "$headers_file"
  poll_status="$status"
  poll_body="$body"
}

RESULT=""
polls_done=0
# Safety cap on total loop iterations (including 429 backoffs) so a
# persistently rate-limited run can't spin forever.
max_iterations=$((MAX_POLLS * 5))
iterations=0

while [ "$polls_done" -lt "$MAX_POLLS" ] && [ "$iterations" -lt "$max_iterations" ]; do
  iterations=$((iterations + 1))
  retry_after=""
  poll_job

  if [ "$poll_status" = "429" ]; then
    wait_secs="${retry_after:-$POLL_INTERVAL}"
    debug "Poll rate limited (429), backing off ${wait_secs}s without spending a poll attempt"
    sleep "$wait_secs"
    continue
  fi

  polls_done=$((polls_done + 1))

  if [ -z "$poll_body" ]; then
    sleep $POLL_INTERVAL
    continue
  fi

  RESULT="$poll_body"
  DONE=$(echo "$RESULT" | jq -r '.done')
  debug "Poll $polls_done: done=$DONE"

  if [ "$DONE" = "true" ]; then
    break
  fi

  sleep $POLL_INTERVAL
done

if [ -z "$RESULT" ]; then
  notify "Connection lost" "Upload succeeded but lost connection while waiting for results."
  exit 0
fi

STATUS=$(echo "$RESULT" | jq -r '.status')
debug "STATUS=$STATUS"
debug "Full result: $RESULT"

# Rejected is not failed. A rejection is a decision about the document and
# repeating the upload reproduces it exactly, so saying "Processing failed"
# here invited retry loops that re-ran a paid model on a file that was never
# going to be accepted. The reason comes from rejection_message.
if [ "$STATUS" = "rejected" ]; then
  REASON=$(echo "$RESULT" | jq -r '.rejection_message // .rejection_reason // "content moderation"')
  notify "Rejected: $REASON" "$(basename "$FILE") was not accepted. Retrying the same file will not change this. Adjust your moderation level in Settings if this looks wrong."
  exit 2
fi

if [ "$STATUS" != "forwarded" ]; then
  ERROR=$(echo "$RESULT" | jq -r '.error // "Processing failed"')
  notify "Processing failed" "$ERROR"
  exit 1
fi

# ── Extract metadata ──────────────────────────────────────────
debug "Extracting metadata..."
TITLE=$(echo "$RESULT" | jq -r '.title // empty')
SUMMARY=$(echo "$RESULT" | jq -r '.summary // empty')
DOC_TYPE=$(echo "$RESULT" | jq -r '.doc_type // empty')
ISSUER=$(echo "$RESULT" | jq -r '.issuer // empty')
DATES=$(echo "$RESULT" | jq -r '.dates // empty')
# Build description for Finder comment
COMMENT="$SUMMARY"
[ -n "$DOC_TYPE" ] && COMMENT="[$DOC_TYPE] $COMMENT"
[ -n "$ISSUER" ] && COMMENT="From: $ISSUER | $COMMENT"
[ -n "$DATES" ] && COMMENT="$COMMENT | Date: $DATES"

# ── Write Finder comment (visible in Get Info > Comments) ─────
# Strip HTML, write to temp file, let AppleScript read it (avoids all quoting issues)
if [ -n "$COMMENT" ]; then
  TMPCOMMENT=$(mktemp)
  echo "$COMMENT" | sed 's/<[^>]*>//g' > "$TMPCOMMENT"
  osascript \
    -e 'set theComment to read (POSIX file "'"$TMPCOMMENT"'") as «class utf8»' \
    -e 'set posixFile to POSIX file "'"$FILE"'"' \
    -e 'tell application "Finder" to set comment of (posixFile as alias) to theComment'
  rm -f "$TMPCOMMENT"
fi

# ── Add Finder tags ───────────────────────────────────────────
if [ "$ADD_TAGS" = "true" ]; then
  TAGS=$(echo "$RESULT" | jq -r '.tags[]? // empty')
  if [ -n "$TAGS" ] && command -v tag &>/dev/null; then
    while IFS= read -r t; do
      [ -n "$t" ] && tag --add "$t" "$FILE"
    done <<< "$TAGS"
  fi
fi

# ── Rename file ───────────────────────────────────────────────
if [ "$RENAME_FILE" = "true" ] && [ -n "$TITLE" ]; then
  DIR=$(dirname "$FILE")
  EXT="${FILE##*.}"
  SAFE_TITLE=$(echo "$TITLE" | sed 's/[\/\\:*?"<>|]/-/g' | head -c 200)
  debug "Rename: TITLE=$TITLE"
  debug "Rename: EXT=$EXT SAFE_TITLE=$SAFE_TITLE"
  # Strip extension from title if already present (API sometimes includes it)
  SAFE_TITLE="${SAFE_TITLE%.$EXT}"
  debug "Rename: after strip=$SAFE_TITLE"
  NEW_PATH="$DIR/$SAFE_TITLE.$EXT"
  debug "Rename: NEW_PATH=$NEW_PATH"

  if [ ! -e "$NEW_PATH" ] && [ "$FILE" != "$NEW_PATH" ]; then
    debug "Rename: moving $FILE -> $NEW_PATH"
    mv "$FILE" "$NEW_PATH"
    FILE="$NEW_PATH"
  else
    debug "Rename: skipped (exists=$([ -e "$NEW_PATH" ] && echo yes || echo no) same=$([ "$FILE" = "$NEW_PATH" ] && echo yes || echo no))"
  fi
fi

# Mark as processed with timestamp so Hazel doesn't re-trigger
debug "Marking processed: $FILE"
xattr -w com.indecs.processed "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$FILE"

debug "Done: $TITLE"
notify "Done" "$TITLE"
