#!/bin/bash
# Indecs batch processor
# https://indecs.app
#
# Process every supported file in a folder through Indecs.
#
# This script drives scan.sh once per file, so it needs scan.sh present as a
# local, executable file -- it cannot be piped straight from curl the way
# scan.sh can, because there is no scan.sh on disk yet for it to call.
#
# Download both, then run:
#   curl -sfL https://indecs.app/scan.sh -o scan.sh && chmod +x scan.sh
#   curl -sfL https://indecs.app/batch-scan.sh -o batch-scan.sh && chmod +x batch-scan.sh
#   INDECS_API_KEY="idk_your-key-here" ./batch-scan.sh /path/to/folder
#
# Usage:
#   ./batch-scan.sh /path/to/folder
#   ./batch-scan.sh --force /path/to/folder    # re-process already-tagged files
#   ./batch-scan.sh Screenshot*.png            # process a specific list of files
#
# Features:
#   - Skips files that already have Indecs metadata (override with --force)
#   - Progress bar with ETA
#   - Resumes where it left off if interrupted (Ctrl-C safe)
#   - Logs errors to batch-scan-errors.log in the target folder
#
# INDECS_API_KEY on the command line lands in shell history. See scan.sh's
# header for the ~/.indecs alternative if that's a problem for you.
#
# Dependencies: scan.sh (must be in the same directory, or set SCAN_SH), jq

set -uo pipefail

# ── Parse arguments ───────────────────────────────────────────
FORCE=false
INPUTS=()

for arg in "$@"; do
  case "$arg" in
    --force|-f) FORCE=true ;;
    -*) echo "Unknown flag: $arg" >&2; exit 1 ;;
    *)
      if [ -e "$arg" ]; then
        INPUTS+=("$arg")
      else
        # Quoted glob? Expand it ourselves. Setting IFS to newline keeps word
        # splitting from breaking on spaces in paths; nullglob makes an
        # unmatched pattern expand to nothing instead of staying literal.
        saved_ifs="$IFS"
        IFS=$'\n'
        shopt -s nullglob
        matches=($arg)
        shopt -u nullglob
        IFS="$saved_ifs"
        if [ ${#matches[@]} -eq 0 ]; then
          echo "No such file or directory: $arg" >&2
          exit 1
        fi
        for expanded in "${matches[@]}"; do
          INPUTS+=("$expanded")
        done
      fi
      ;;
  esac
done

if [ ${#INPUTS[@]} -eq 0 ]; then
  echo "Usage: batch-scan.sh [--force] /path/to/folder" >&2
  echo "       batch-scan.sh [--force] file1 file2 ..." >&2
  exit 1
fi

# Folder mode: single arg that is a directory. Otherwise file-list mode.
FOLDER=""
FILE_LIST_MODE=false
if [ ${#INPUTS[@]} -eq 1 ] && [ -d "${INPUTS[0]}" ]; then
  FOLDER="${INPUTS[0]}"
else
  FILE_LIST_MODE=true
  for f in "${INPUTS[@]}"; do
    if [ ! -f "$f" ]; then
      echo "Not a file: $f" >&2
      exit 1
    fi
  done
  # State and log files go next to the first input.
  FOLDER="$(cd "$(dirname "${INPUTS[0]}")" && pwd)"
fi

# ── Locate scan.sh ───────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCAN_SH="${SCAN_SH:-$SCRIPT_DIR/scan.sh}"

if [ ! -x "$SCAN_SH" ]; then
  echo "scan.sh not found at $SCAN_SH (set SCAN_SH env var to override)" >&2
  exit 1
fi

# ── Supported extensions ─────────────────────────────────────
EXTENSIONS="pdf jpg jpeg png webp gif heic heif avif bmp"

# ── Build file list ──────────────────────────────────────────
ALL_FILES=()

if [ "$FILE_LIST_MODE" = true ]; then
  # Trust the caller's explicit list; scan.sh will reject unsupported types.
  for f in "${INPUTS[@]}"; do
    ALL_FILES+=("$f")
  done
else
  FIND_ARGS=()
  first=true
  for ext in $EXTENSIONS; do
    if [ "$first" = true ]; then
      FIND_ARGS+=(-name "*.$ext")
      first=false
    else
      FIND_ARGS+=(-o -name "*.$ext")
    fi
  done

  while IFS= read -r -d '' f; do
    ALL_FILES+=("$f")
  done < <(find "$FOLDER" -maxdepth 1 -type f \( "${FIND_ARGS[@]}" \) -print0 | sort -z)
fi

if [ ${#ALL_FILES[@]} -eq 0 ]; then
  echo "No supported files found in $FOLDER"
  exit 0
fi

# ── Filter already-processed files ───────────────────────────
TODO=()
SKIPPED=0

for f in "${ALL_FILES[@]}"; do
  if [ "$FORCE" = true ]; then
    TODO+=("$f")
  elif xattr -p com.indecs.processed "$f" &>/dev/null; then
    SKIPPED=$((SKIPPED + 1))
  else
    TODO+=("$f")
  fi
done

TOTAL=${#TODO[@]}

if [ "$TOTAL" -eq 0 ]; then
  echo "All ${#ALL_FILES[@]} files already processed. Use --force to re-process."
  exit 0
fi

if [ "$SKIPPED" -gt 0 ]; then
  echo "Skipping $SKIPPED already-processed files (use --force to include them)"
fi

# ── State file for resume ────────────────────────────────────
STATE_FILE="$FOLDER/.batch-scan-state"
LOG_FILE="$FOLDER/batch-scan-errors.log"
DONE_COUNT=0
FAIL_COUNT=0
REJECT_COUNT=0
PAUSED=0
START_TIME=$(date +%s)

# Load resume state if it exists (state file is a newline-separated list of
# already-completed paths; we grep it per-file rather than using an associative
# array, so this stays compatible with macOS's bash 3.2).
if [ -f "$STATE_FILE" ]; then
  RESUMED=$(wc -l < "$STATE_FILE" | tr -d ' ')
  if [ "$RESUMED" -gt 0 ]; then
    echo "Resuming - $RESUMED files already done from previous run"
  fi
fi

# ── Clean shutdown on Ctrl-C ─────────────────────────────────
cleanup() {
  echo ""
  echo "Interrupted. $DONE_COUNT/$TOTAL completed, $FAIL_COUNT failed."
  echo "Run again to resume from where you left off."
  exit 130
}
trap cleanup INT TERM

# ── Progress bar ─────────────────────────────────────────────
progress() {
  local current=$1 total=$2 filename=$3
  local pct=$((current * 100 / total))
  local filled=$((pct / 2))
  local empty=$((50 - filled))
  local bar=$(printf '%0.s#' $(seq 1 $filled 2>/dev/null))
  local space=$(printf '%0.s-' $(seq 1 $empty 2>/dev/null))

  # ETA calculation
  local elapsed=$(( $(date +%s) - START_TIME ))
  local eta=""
  if [ "$current" -gt 0 ] && [ "$elapsed" -gt 0 ]; then
    local remaining=$(( (elapsed * (total - current)) / current ))
    if [ "$remaining" -ge 60 ]; then
      eta="ETA $(( remaining / 60 ))m$(( remaining % 60 ))s"
    else
      eta="ETA ${remaining}s"
    fi
  fi

  printf "\r[%-50s] %3d%% (%d/%d) %s  %-40.40s" "$bar" "$pct" "$current" "$total" "$eta" "$filename"
}

# ── Process files ────────────────────────────────────────────
echo "Processing $TOTAL files in $FOLDER"
echo "Errors logged to $LOG_FILE"
echo ""

INDEX=0

for f in "${TODO[@]}"; do
  INDEX=$((INDEX + 1))
  BASENAME=$(basename "$f")

  # Skip if already completed in a previous run
  if [ -f "$STATE_FILE" ] && grep -Fxq -- "$f" "$STATE_FILE"; then
    DONE_COUNT=$((DONE_COUNT + 1))
    progress $INDEX $TOTAL "(resumed) $BASENAME"
    continue
  fi

  progress $INDEX $TOTAL "$BASENAME"

  # Clear the processed xattr if --force so scan.sh doesn't skip it
  if [ "$FORCE" = true ]; then
    xattr -d com.indecs.processed "$f" 2>/dev/null || true
  fi

  # Run scan.sh and capture output
  OUTPUT=$("$SCAN_SH" "$f" 2>&1)
  EXIT_CODE=$?

  if [ $EXIT_CODE -eq 0 ]; then
    DONE_COUNT=$((DONE_COUNT + 1))
    echo "$f" >> "$STATE_FILE"
  elif [ $EXIT_CODE -eq 2 ]; then
    # Exit 2 from scan.sh means the document was rejected by content
    # moderation. That is a decision about the file, not a transient error:
    # re-uploading it reproduces the same verdict and bills another model run.
    # Recorded in the state file so a resume skips it, and counted apart from
    # failures so the summary does not imply retrying would help.
    REJECT_COUNT=$((REJECT_COUNT + 1))
    echo "$f" >> "$STATE_FILE"
    echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] REJECTED $f" >> "$LOG_FILE"
    if [ -n "$OUTPUT" ]; then
      echo "  $OUTPUT" >> "$LOG_FILE"
    fi
  elif [ $EXIT_CODE -eq 3 ]; then
    # Exit 3 means uploads are paused for the whole account, not that this
    # file was refused. Every remaining file would collect the same 403, so
    # stop rather than grind through them. This file is not recorded as done,
    # so a resume after the pause expires picks up exactly where it stopped.
    PAUSED=1
    echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] PAUSED $f" >> "$LOG_FILE"
    if [ -n "$OUTPUT" ]; then
      echo "  $OUTPUT" >> "$LOG_FILE"
    fi
    break
  else
    FAIL_COUNT=$((FAIL_COUNT + 1))
    echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] FAIL (exit $EXIT_CODE) $f" >> "$LOG_FILE"
    if [ -n "$OUTPUT" ]; then
      echo "  $OUTPUT" >> "$LOG_FILE"
    fi
  fi
done

# ── Summary ──────────────────────────────────────────────────
echo ""
echo ""

ELAPSED=$(( $(date +%s) - START_TIME ))
if [ "$ELAPSED" -ge 60 ]; then
  TIME_STR="$(( ELAPSED / 60 ))m$(( ELAPSED % 60 ))s"
else
  TIME_STR="${ELAPSED}s"
fi

echo "Done in $TIME_STR: $DONE_COUNT succeeded, $REJECT_COUNT rejected, $FAIL_COUNT failed, $SKIPPED skipped"

if [ "$PAUSED" -eq 1 ]; then
  echo "Stopped early: uploads are paused after repeated rejected documents."
  echo "Re-run this command once the pause expires to continue where it stopped."
fi

if [ "$FAIL_COUNT" -gt 0 ]; then
  echo "See $LOG_FILE for error details"
fi

# Clean up state file on successful completion (no failures)
if [ "$FAIL_COUNT" -eq 0 ]; then
  rm -f "$STATE_FILE"
fi
