#!/bin/sh
# relay — dependency-free client for the Relay HTTP file relay.
#
# Fetch and run directly from the service:
#   curl -fsSL https://<host>/relay.sh -o relay && chmod +x relay
#   ./relay config --service-url https://<host> --user <name>
#
# Requires only `curl` and POSIX utilities. No install step, no runtime deps.
# Exit codes: 0 ok | 1 usage/4xx | 2 local config | 4 401 | 5 403 | 6 404
#             7 409 | 8 410 | 9 413 | 10 5xx/malformed | 20 transport.

set -u

RELAY_DIR="${RELAY_HOME:-$HOME/.relay}"
CONFIG_FILE="$RELAY_DIR/config"
CREDS_FILE="$RELAY_DIR/credentials"
DEFAULT_URL="https://relay.samyx.net"

# Flag/positional state.
CHANNEL_ID="" NAME="" BUFFER=0 LISTEN=0 MULTI=0 OUTPUT="" TOKEN=""
SET_URL="" SET_USER="" USERNAME="" CLI_PASS="" ADMIN_FLAG=0
BUFFER_SIZE="" CHANNEL_LIMIT="" TIMEOUT="" SHOW=0
POS1="" POS2=""

_error() { printf 'relay: %s\n' "$1" >&2; exit "${2:-1}"; }

_need_curl() { command -v curl >/dev/null 2>&1 || _error "curl is required but not found" 2; }

_mktemp() { mktemp 2>/dev/null || mktemp -t relay 2>/dev/null; }

_json_field() { # $1=key; reads JSON from stdin, prints first flat string value
	sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1
}

_json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }

_load_config() {
	# DEFAULT_URL is substituted with the real host when the script is served.
	# If it still equals the placeholder (script run un-served), ignore it.
	# The marker is split so the server's string substitution never rewrites
	# this comparison, only the DEFAULT_URL assignment above.
	_ph='__RELAY''_DEFAULT_URL__'
	SERVICE_URL=""
	[ "$DEFAULT_URL" != "$_ph" ] && SERVICE_URL="$DEFAULT_URL"
	CFG_USER=""
	if [ -f "$CONFIG_FILE" ]; then
		while IFS='=' read -r k v; do
			case "$k" in
			service_url) SERVICE_URL="$v" ;;
			username) CFG_USER="$v" ;;
			esac
		done <"$CONFIG_FILE"
	fi
	[ -n "${RELAY_URL:-}" ] && SERVICE_URL="$RELAY_URL"
}

_load_creds() {
	FILE_USER="" FILE_PASS=""
	if [ -f "$CREDS_FILE" ]; then
		line=$(head -n1 "$CREDS_FILE")
		FILE_USER=${line%%:*}
		FILE_PASS=${line#*:}
	fi
	# precedence: env -> file -> config username
	USER="${RELAY_USER:-${FILE_USER:-$CFG_USER}}"
	PASS="${RELAY_PASSWORD:-$FILE_PASS}"
}

_require_url() { [ -n "$SERVICE_URL" ] || _error "no service URL; run: relay config --service-url <url>" 2; }

# _request METHOD PATH [curl args...] -> sets HTTP_CODE and BODY_FILE.
# Uses a capability token (X-Relay-Token, no account) when $TOKEN is set,
# otherwise HTTP Basic Auth.
_request() {
	_m=$1
	_p=$2
	shift 2
	_require_url
	BODY_FILE=$(_mktemp) || _error "cannot create temp file" 2
	if [ -n "$TOKEN" ]; then
		set -- -H "X-Relay-Token: $TOKEN" "$@"
	else
		set -- -u "$USER:$PASS" "$@"
	fi
	HTTP_CODE=$(curl -sS -X "$_m" -o "$BODY_FILE" -w '%{http_code}' "$@" "$SERVICE_URL$_p")
	_rc=$?
	if [ "$_rc" -ne 0 ]; then
		rm -f "$BODY_FILE"
		_error "connection failed (curl exit $_rc)" 20
	fi
}

# _check maps an error HTTP_CODE to a message + exit code, cleaning BODY_FILE.
_check() {
	if [ "$HTTP_CODE" -ge 400 ]; then
		msg=$(_json_field error <"$BODY_FILE")
		[ -z "$msg" ] && msg="HTTP $HTTP_CODE"
		rm -f "$BODY_FILE"
		case "$HTTP_CODE" in
		401) _error "$msg" 4 ;;
		403) _error "$msg" 5 ;;
		404) _error "$msg" 6 ;;
		409) _error "$msg" 7 ;;
		410) _error "$msg" 8 ;;
		413) _error "$msg" 9 ;;
		5*) _error "$msg" 10 ;;
		*) _error "$msg" 1 ;;
		esac
	fi
}

_prompt_password() { # -> PROMPT_PASS
	printf '%s' "${1:-Password: }" >&2
	if [ -e /dev/tty ]; then
		trap 'stty echo 2>/dev/null' EXIT INT TERM HUP
		stty -echo 2>/dev/null
		IFS= read -r PROMPT_PASS </dev/tty
		stty echo 2>/dev/null
		trap - EXIT INT TERM HUP
		printf '\n' >&2
	else
		IFS= read -r PROMPT_PASS
	fi
}

# --- commands ---------------------------------------------------------------

cmd_config() {
	mkdir -p "$RELAY_DIR" 2>/dev/null
	chmod 700 "$RELAY_DIR" 2>/dev/null
	[ -n "$SET_URL" ] && SERVICE_URL="$SET_URL"
	[ -n "$SERVICE_URL" ] || _error "config requires --service-url" 1
	{
		printf 'service_url=%s\n' "$SERVICE_URL"
		[ -n "$SET_USER" ] && printf 'username=%s\n' "$SET_USER"
	} >"$CONFIG_FILE"
	chmod 600 "$CONFIG_FILE" 2>/dev/null
	if [ -n "$SET_USER" ]; then
		_prompt_password "Password for $SET_USER: "
		USER="$SET_USER"
		PASS="$PROMPT_PASS"
		_request GET /me
		if [ "$HTTP_CODE" != 200 ]; then
			rm -f "$BODY_FILE"
			_error "credential check failed (HTTP $HTTP_CODE)" 4
		fi
		rm -f "$BODY_FILE"
		(umask 077 && printf '%s:%s\n' "$SET_USER" "$PROMPT_PASS" >"$CREDS_FILE")
		chmod 600 "$CREDS_FILE" 2>/dev/null
		printf 'credentials saved for %s\n' "$SET_USER" >&2
	fi
	printf 'config saved (%s)\n' "$SERVICE_URL" >&2
}

cmd_channel_create() {
	q=""
	[ "$BUFFER" = 1 ] && q="buffer=true"
	if [ -n "$NAME" ]; then
		[ -n "$q" ] && q="$q&name=$NAME" || q="name=$NAME"
	fi
	p="/channels"
	[ -n "$q" ] && p="/channels?$q"
	_request POST "$p"
	_check
	id=$(_json_field id <"$BODY_FILE")
	st=$(_json_field send_token <"$BODY_FILE")
	rt=$(_json_field receive_token <"$BODY_FILE")
	rm -f "$BODY_FILE"
	[ -n "$id" ] || _error "server did not return a channel id" 10
	if [ -n "$st" ]; then
		printf 'send token:    %s\n' "$st" >&2
		printf 'receive token: %s   (share out-of-band; no account needed)\n' "$rt" >&2
	fi
	if [ "$LISTEN" = 1 ]; then
		CHANNEL_ID="$id"
		printf 'channel %s — listening...\n' "$id" >&2
		cmd_channel_listen
	else
		printf '%s\n' "$id"
	fi
}

cmd_channel_send() {
	[ -n "$POS1" ] || _error "send requires a file argument" 1
	[ -f "$POS1" ] || _error "no such file: $POS1" 2
	fn=$(basename "$POS1")
	if [ -n "$TOKEN" ]; then
		_p="/shared"
	else
		[ -n "$CHANNEL_ID" ] || _error "send requires --channel-id or --token" 1
		_p="/channels/$CHANNEL_ID"
	fi
	_request POST "$_p" -T "$POS1" -H "X-Relay-Filename: $fn"
	_check
	st=$(_json_field status <"$BODY_FILE")
	rm -f "$BODY_FILE"
	printf 'sent %s (%s)\n' "$fn" "${st:-ok}" >&2
}

_pull_once() { # returns 0=got file, 2=empty(204), 3=closed(410)
	_require_url
	TMP=$(_mktemp) || _error "cannot create temp file" 2
	HDR=$(_mktemp) || _error "cannot create temp file" 2
	if [ -n "$TOKEN" ]; then
		set -- -H "X-Relay-Token: $TOKEN"
		_pp="/shared"
	else
		set -- -u "$USER:$PASS"
		_pp="/channels/$CHANNEL_ID"
	fi
	HTTP_CODE=$(curl -sS -N "$@" -D "$HDR" -o "$TMP" -w '%{http_code}' "$SERVICE_URL$_pp")
	_rc=$?
	if [ "$_rc" -ne 0 ]; then
		rm -f "$TMP" "$HDR"
		_error "connection failed (curl exit $_rc)" 20
	fi
	case "$HTTP_CODE" in
	200)
		if [ "$OUTPUT" = "-" ]; then
			cat "$TMP"
		else
			fn=$(sed -n 's/.*filename="\([^"]*\)".*/\1/p' "$HDR" | head -n1 | tr -d '\r')
			[ -n "$fn" ] || fn="relay-download"
			fn=$(basename "$fn")
			[ -n "$OUTPUT" ] && fn="$OUTPUT"
			dest="$fn"
			[ -e "$dest" ] && dest="$dest.$$"
			mv "$TMP" "$dest"
			printf 'received: %s\n' "$dest" >&2
			TMP=""
		fi
		rm -f "$TMP" "$HDR"
		return 0
		;;
	204)
		rm -f "$TMP" "$HDR"
		return 2
		;;
	410)
		rm -f "$TMP" "$HDR"
		return 3
		;;
	*)
		msg=$(_json_field error <"$TMP")
		rm -f "$TMP" "$HDR"
		_error "${msg:-HTTP $HTTP_CODE}" 10
		;;
	esac
}

cmd_channel_listen() {
	[ -n "$CHANNEL_ID" ] || [ -n "$TOKEN" ] || _error "listen requires --channel-id or --token" 1
	while :; do
		_pull_once
		r=$?
		case $r in
		0) [ "$MULTI" = 1 ] && continue || break ;;
		2) continue ;; # long-poll window elapsed; keep waiting
		3)
			[ "$MULTI" = 1 ] && printf 'channel closed\n' >&2
			break
			;;
		esac
	done
}

cmd_channel_cancel() {
	[ -n "$CHANNEL_ID" ] || _error "cancel requires --channel-id" 1
	_request DELETE "/channels/$CHANNEL_ID"
	_check
	rm -f "$BODY_FILE"
	printf 'cancelled %s\n' "$CHANNEL_ID" >&2
}

cmd_status() {
	if [ -n "$TOKEN" ]; then
		_p="/shared/status"
	else
		[ -n "$CHANNEL_ID" ] || _error "status requires --channel-id or --token" 1
		_p="/channels/$CHANNEL_ID/status"
	fi
	_request GET "$_p"
	_check
	cat "$BODY_FILE"
	printf '\n'
	rm -f "$BODY_FILE"
}

cmd_permissions_buffer() {
	p="/permissions/buffer"
	[ -n "$CHANNEL_ID" ] && p="$p?channel-id=$CHANNEL_ID"
	_request GET "$p"
	_check
	cat "$BODY_FILE"
	printf '\n'
	rm -f "$BODY_FILE"
}

cmd_me() {
	_request GET /me
	_check
	cat "$BODY_FILE"
	rm -f "$BODY_FILE"
}

cmd_me_update() {
	_prompt_password "Current password: "
	old="$PROMPT_PASS"
	_prompt_password "New password: "
	new="$PROMPT_PASS"
	body=$(printf '{"old_password":"%s","new_password":"%s"}' "$(_json_escape "$old")" "$(_json_escape "$new")")
	_request POST /me/password --data "$body" -H 'Content-Type: application/json'
	_check
	rm -f "$BODY_FILE"
	printf 'password updated\n' >&2
}

cmd_admin_users_create() {
	[ -n "$USERNAME" ] || _error "requires --username" 1
	if [ -z "$CLI_PASS" ]; then
		_prompt_password "Password for $USERNAME: "
		CLI_PASS="$PROMPT_PASS"
	fi
	extra=""
	[ "$ADMIN_FLAG" = 1 ] && extra=',"admin":true'
	body=$(printf '{"username":"%s","password":"%s"%s}' "$USERNAME" "$(_json_escape "$CLI_PASS")" "$extra")
	_request POST /admin/users --data "$body" -H 'Content-Type: application/json'
	_check
	rm -f "$BODY_FILE"
	printf 'user created: %s\n' "$USERNAME" >&2
}

cmd_admin_users_list() {
	_request GET /admin/users
	_check
	cat "$BODY_FILE"
	rm -f "$BODY_FILE"
}

cmd_admin_users_delete() {
	[ -n "$USERNAME" ] || _error "requires --username" 1
	_request DELETE "/admin/users/$USERNAME"
	_check
	rm -f "$BODY_FILE"
	printf 'deleted %s\n' "$USERNAME" >&2
}

cmd_admin_quota() {
	[ -n "$USERNAME" ] || _error "requires --username" 1
	if [ "$SHOW" = 1 ]; then
		_request GET "/admin/users/$USERNAME/quota"
		_check
		cat "$BODY_FILE"
		printf '\n'
		rm -f "$BODY_FILE"
		return
	fi
	parts=""
	[ -n "$BUFFER_SIZE" ] && parts="$parts\"buffer_size_limit\":$BUFFER_SIZE,"
	[ -n "$CHANNEL_LIMIT" ] && parts="$parts\"channel_limit\":$CHANNEL_LIMIT,"
	[ -n "$TIMEOUT" ] && parts="$parts\"timeout_seconds\":$TIMEOUT,"
	parts=${parts%,}
	_request PATCH "/admin/users/$USERNAME/quota" --data "{$parts}" -H 'Content-Type: application/json'
	_check
	cat "$BODY_FILE"
	printf '\n'
	rm -f "$BODY_FILE"
}

usage() {
	cat >&2 <<'EOF'
relay — HTTP file relay client

  relay config --service-url <url> [--user <name>]
  relay channel create [--listen] [--buffer] [--name <name>]
  relay channel send --channel-id <id> <file>
  relay channel listen --channel-id <id> [--multi] [--output <file|->]
  relay channel cancel --channel-id <id>
  relay status --channel-id <id>

  # Sharing with others (no account needed — just curl + a token):
  relay channel send   --token <send-token> <file>
  relay channel listen --token <recv-token> [--multi] [--output <file|->]
  relay permissions buffer [--channel-id <id>]
  relay me
  relay me update --password
  relay admin users create --username <name> [--password <pw>] [--admin]
  relay admin users list
  relay admin users delete --username <name>
  relay admin quota --username <name> [--buffer-size <bytes>] [--channel-limit <n>] [--timeout <s>] [--show]

Env: RELAY_URL, RELAY_USER, RELAY_PASSWORD override config/credentials.
EOF
	exit "${1:-1}"
}

# --- argument parsing -------------------------------------------------------

_need_curl

GROUP=${1:-}
[ $# -gt 0 ] && shift
ACTION=""
case "$GROUP" in
status | config) : ;;
"" | -h | --help | help) usage 0 ;;
*)
	case "${1:-}" in
	"" | -*) : ;;
	*)
		ACTION=$1
		shift
		;;
	esac
	;;
esac

while [ $# -gt 0 ]; do
	case "$1" in
	--channel-id) CHANNEL_ID=${2:-}; shift 2 ;;
	--token) TOKEN=${2:-}; shift 2 ;;
	--name) NAME=${2:-}; shift 2 ;;
	--buffer) BUFFER=1; shift ;;
	--listen) LISTEN=1; shift ;;
	--multi) MULTI=1; shift ;;
	--output) OUTPUT=${2:-}; shift 2 ;;
	--service-url) SET_URL=${2:-}; shift 2 ;;
	--user) SET_USER=${2:-}; shift 2 ;;
	--username) USERNAME=${2:-}; shift 2 ;;
	--password) CLI_PASS=${2:-}; shift 2 ;;
	--admin) ADMIN_FLAG=1; shift ;;
	--buffer-size) BUFFER_SIZE=${2:-}; shift 2 ;;
	--channel-limit) CHANNEL_LIMIT=${2:-}; shift 2 ;;
	--timeout) TIMEOUT=${2:-}; shift 2 ;;
	--show) SHOW=1; shift ;;
	-*) _error "unknown flag: $1" 1 ;;
	*)
		if [ -z "$POS1" ]; then POS1=$1; elif [ -z "$POS2" ]; then POS2=$1; fi
		shift
		;;
	esac
done

_load_config
_load_creds

case "$GROUP" in
channel)
	case "$ACTION" in
	create) cmd_channel_create ;;
	send) cmd_channel_send ;;
	listen) cmd_channel_listen ;;
	cancel) cmd_channel_cancel ;;
	*) usage ;;
	esac
	;;
permissions)
	case "$ACTION" in
	buffer) cmd_permissions_buffer ;;
	*) usage ;;
	esac
	;;
status) cmd_status ;;
config) cmd_config ;;
me)
	case "$ACTION" in
	"") cmd_me ;;
	update) cmd_me_update ;;
	*) usage ;;
	esac
	;;
admin)
	case "$ACTION" in
	users)
		case "$POS1" in
		create) cmd_admin_users_create ;;
		list) cmd_admin_users_list ;;
		delete) cmd_admin_users_delete ;;
		*) usage ;;
		esac
		;;
	quota) cmd_admin_quota ;;
	*) usage ;;
	esac
	;;
*) usage ;;
esac
