#!/bin/sh

. /usr/share/libubox/jshn.sh

BASE_DIR="/tmp/luci-app-run"
UPLOAD_DIR="$BASE_DIR/uploads"
RUN_DIR="$BASE_DIR/runs"
STATE_FILE="$BASE_DIR/state"
MAX_SIZE=$((256 * 1024 * 1024))

umask 077

mkdir -p "$UPLOAD_DIR" "$RUN_DIR"

reply() {
	printf '%s\n' "$1"
}

json_error() {
	local msg="$1"
	json_init
	json_add_int code 1
	json_add_string error "$msg"
	json_dump
}

json_ok() {
	json_init
	json_add_int code 0
	[ -n "$1" ] && json_add_string message "$1"
	json_dump
}

sanitize_name() {
	local name="$1"
	name="${name##*/}"
	name="${name##*\\}"
	printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_'
}

valid_id() {
	case "$1" in
		""|*[!A-Za-z0-9_-]*) return 1 ;;
	esac
	return 0
}

load_input() {
	local input
	input="$(cat)"
	json_load "$input" >/dev/null 2>&1
}

get_string() {
	json_get_var "$1" "$1"
}

get_int() {
	json_get_var "$1" "$1"
}

new_token() {
	local token
	token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)"
	[ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')"
	printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_'
}

session_dir() {
	printf '%s/%s' "$UPLOAD_DIR" "$1"
}

write_state() {
	local pid="$1"
	local file="$2"
	local log="$3"
	local started="$4"
	{
		printf 'PID=%s\n' "$pid"
		printf 'FILE=%s\n' "$file"
		printf 'LOG=%s\n' "$log"
		printf 'STARTED=%s\n' "$started"
	} > "$STATE_FILE"
}

read_state() {
	[ -f "$STATE_FILE" ] && . "$STATE_FILE"
}

is_running() {
	local pid="$1"
	[ -n "$pid" ] && [ -d "/proc/$pid" ]
}

method_list() {
	cat <<'JSON'
{
	"upload_start": {
		"filename": "String",
		"size": "Integer"
	},
	"upload_chunk": {
		"id": "String",
		"data": "String",
		"index": "Integer"
	},
	"upload_finish": {
		"id": "String"
	},
	"run": {
		"id": "String"
	},
	"status": {},
	"read_log": {
		"offset": "Integer"
	},
	"cleanup": {}
}
JSON
}

upload_start() {
	load_input
	get_string filename
	get_int size

	[ -n "$filename" ] || { json_error "Missing filename"; return; }
	[ -n "$size" ] || size=0
	[ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; }

	local clean id token dir file
	clean="$(sanitize_name "$filename")"
	case "$clean" in
		*.run) ;;
		*) json_error "Only .run files are accepted"; return ;;
	esac

	id="$(date +%s)_$$"
	token="$(new_token)"
	dir="$(session_dir "$id")"
	file="$dir/$clean"
	mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; }
	: > "$file" || { json_error "Unable to create upload file"; return; }
	printf '%s\n' "$clean" > "$dir/name"
	printf '%s\n' "$size" > "$dir/size"
	printf '%s\n' "$token" > "$dir/token"

	json_init
	json_add_int code 0
	json_add_string id "$id"
	json_add_string token "$token"
	json_add_string filename "$clean"
	json_dump
}

upload_chunk() {
	load_input
	get_string id
	get_string data
	get_int index

	valid_id "$id" || { json_error "Invalid upload id"; return; }
	[ -n "$data" ] || { json_error "Missing chunk data"; return; }

	local dir name file
	dir="$(session_dir "$id")"
	[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
	name="$(cat "$dir/name" 2>/dev/null)"
	file="$dir/$name"

	printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || {
		json_error "Unable to decode upload chunk"
		return
	}

	json_init
	json_add_int code 0
	json_add_int index "$index"
	json_add_int received "$(wc -c < "$file" 2>/dev/null)"
	json_dump
}

upload_finish() {
	load_input
	get_string id

	valid_id "$id" || { json_error "Invalid upload id"; return; }

	local dir name file expected actual
	dir="$(session_dir "$id")"
	[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
	name="$(cat "$dir/name" 2>/dev/null)"
	expected="$(cat "$dir/size" 2>/dev/null)"
	file="$dir/$name"
	actual="$(wc -c < "$file" 2>/dev/null)"

	[ -s "$file" ] || { json_error "Uploaded file is empty"; return; }
	if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then
		[ "$actual" -eq "$expected" ] 2>/dev/null || {
			json_error "Upload size mismatch"
			return
		}
	fi

	chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; }

	json_init
	json_add_int code 0
	json_add_string id "$id"
	json_add_string filename "$name"
	json_add_int size "$actual"
	json_dump
}

run_installer() {
	load_input
	get_string id

	valid_id "$id" || { json_error "Invalid upload id"; return; }

	local dir name file log work pid now
	dir="$(session_dir "$id")"
	[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
	name="$(cat "$dir/name" 2>/dev/null)"
	file="$dir/$name"
	[ -x "$file" ] || { json_error "Uploaded file is not executable"; return; }

	read_state
	if is_running "$PID"; then
		json_error "Another installer is already running"
		return
	fi

	now="$(date '+%Y-%m-%d %H:%M:%S')"
	work="$RUN_DIR/$id"
	log="$work/output.log"
	mkdir -p "$work" || { json_error "Unable to create run directory"; return; }

	(
		cd "$work" || exit 1
		printf 'luci-app-run: started %s\n' "$now"
		printf 'luci-app-run: executing %s\n\n' "$file"
		"$file"
		rc=$?
		printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
		exit "$rc"
	) > "$log" 2>&1 &

	pid="$!"
	write_state "$pid" "$file" "$log" "$now"

	json_init
	json_add_int code 0
	json_add_int pid "$pid"
	json_add_string log "$log"
	json_dump
}

status() {
	read_state

	json_init
	json_add_int code 0
	if is_running "$PID"; then
		json_add_boolean running 1
	else
		json_add_boolean running 0
	fi
	[ -n "$PID" ] && json_add_int pid "$PID"
	[ -n "$FILE" ] && json_add_string file "$FILE"
	[ -n "$LOG" ] && json_add_string log "$LOG"
	[ -n "$STARTED" ] && json_add_string started "$STARTED"
	json_dump
}

read_log() {
	load_input
	get_int offset
	[ -n "$offset" ] || offset=0

	read_state
	[ -n "$LOG" ] && [ -f "$LOG" ] || {
		json_init
		json_add_int code 0
		json_add_string data ""
		json_add_int offset 0
		json_add_boolean running 0
		json_dump
		return
	}

	local size data start
	size="$(wc -c < "$LOG" 2>/dev/null)"
	[ "$offset" -le "$size" ] 2>/dev/null || offset=0
	start=$((offset + 1))
	data="$(tail -c +"$start" "$LOG" 2>/dev/null)"

	json_init
	json_add_int code 0
	json_add_string data "$data"
	json_add_int offset "$size"
	if is_running "$PID"; then
		json_add_boolean running 1
	else
		json_add_boolean running 0
	fi
	json_dump
}

cleanup() {
	read_state
	if is_running "$PID"; then
		json_error "Cannot clean up while installer is running"
		return
	fi

	rm -rf "$UPLOAD_DIR" "$RUN_DIR" "$STATE_FILE"
	mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
	json_ok "Cleaned"
}

case "$1" in
	list)
		method_list
	;;
	call)
		case "$2" in
			upload_start) upload_start ;;
			upload_chunk) upload_chunk ;;
			upload_finish) upload_finish ;;
			run) run_installer ;;
			status) status ;;
			read_log) read_log ;;
			cleanup) cleanup ;;
			*) json_error "Unknown method" ;;
		esac
	;;
	*)
		reply '{}'
	;;
esac
